From e96ff688b364d3bcf276c1edeb2760677b2555bc Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 04:43:56 +0200 Subject: [PATCH] update permanent status codes api --- src/client/connection/response/failure.rs | 11 +- .../connection/response/failure/error.rs | 6 +- .../connection/response/failure/permanent.rs | 230 +++++++----------- .../response/failure/permanent/bad_request.rs | 78 ++++++ .../failure/permanent/bad_request/error.rs | 24 ++ .../response/failure/permanent/default.rs | 78 ++++++ .../failure/permanent/default/error.rs | 24 ++ .../response/failure/permanent/error.rs | 45 +++- .../response/failure/permanent/gone.rs | 78 ++++++ .../response/failure/permanent/gone/error.rs | 24 ++ .../response/failure/permanent/not_found.rs | 78 ++++++ .../failure/permanent/not_found/error.rs | 24 ++ .../permanent/proxy_request_refused.rs | 78 ++++++ .../permanent/proxy_request_refused/error.rs | 24 ++ 14 files changed, 644 insertions(+), 158 deletions(-) create mode 100644 src/client/connection/response/failure/permanent/bad_request.rs create mode 100644 src/client/connection/response/failure/permanent/bad_request/error.rs create mode 100644 src/client/connection/response/failure/permanent/default.rs create mode 100644 src/client/connection/response/failure/permanent/default/error.rs create mode 100644 src/client/connection/response/failure/permanent/gone.rs create mode 100644 src/client/connection/response/failure/permanent/gone/error.rs create mode 100644 src/client/connection/response/failure/permanent/not_found.rs create mode 100644 src/client/connection/response/failure/permanent/not_found/error.rs create mode 100644 src/client/connection/response/failure/permanent/proxy_request_refused.rs create mode 100644 src/client/connection/response/failure/permanent/proxy_request_refused/error.rs diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 40c8abf..419af23 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -21,7 +21,7 @@ impl Failure { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.first() { - Some(byte) => match byte { + Some(b) => match b { b'4' => match Temporary::from_utf8(buffer) { Ok(input) => Ok(Self::Temporary(input)), Err(e) => Err(Error::Temporary(e)), @@ -30,7 +30,7 @@ impl Failure { Ok(failure) => Ok(Self::Permanent(failure)), Err(e) => Err(Error::Permanent(e)), }, - _ => Err(Error::Code), + b => Err(Error::Code(*b)), }, None => Err(Error::Protocol), } @@ -38,13 +38,6 @@ impl Failure { // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Permanent(permanent) => permanent.to_code(), - Self::Temporary(temporary) => temporary.to_code(), - } - } - pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), diff --git a/src/client/connection/response/failure/error.rs b/src/client/connection/response/failure/error.rs index 7725b92..056f714 100644 --- a/src/client/connection/response/failure/error.rs +++ b/src/client/connection/response/failure/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, + Code(u8), Permanent(super::permanent::Error), Protocol, Temporary(super::temporary::Error), @@ -11,8 +11,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Code group error") + Self::Code(b) => { + write!(f, "Unexpected status code byte: {b}") } Self::Permanent(e) => { write!(f, "Permanent failure group error: {e}") diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e2ab9e0..ffe4ea3 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -1,24 +1,31 @@ +pub mod bad_request; +pub mod default; pub mod error; -pub use error::Error; +pub mod gone; +pub mod not_found; +pub mod proxy_request_refused; -const DEFAULT: (u8, &str) = (50, "Unspecified"); -const NOT_FOUND: (u8, &str) = (51, "Not found"); -const GONE: (u8, &str) = (52, "Gone"); -const PROXY_REQUEST_REFUSED: (u8, &str) = (53, "Proxy request refused"); -const BAD_REQUEST: (u8, &str) = (59, "bad-request"); +pub use bad_request::BadRequest; +pub use default::Default; +pub use error::Error; +pub use gone::Gone; +pub use not_found::NotFound; +pub use proxy_request_refused::ProxyRequestRefused; + +const CODE: u8 = b'5'; /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure pub enum Permanent { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50 - Default { message: Option }, + Default(Default), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found - NotFound { message: Option }, + NotFound(NotFound), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone - Gone { message: Option }, + Gone(Gone), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused - ProxyRequestRefused { message: Option }, + ProxyRequestRefused(ProxyRequestRefused), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request - BadRequest { message: Option }, + BadRequest(BadRequest), } impl Permanent { @@ -26,154 +33,105 @@ impl Permanent { /// 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::NotFound( + NotFound::from_utf8(buffer).map_err(Error::NotFound)?, + )), + b'2' => Ok(Self::Gone(Gone::from_utf8(buffer).map_err(Error::Gone)?)), + b'3' => Ok(Self::ProxyRequestRefused( + ProxyRequestRefused::from_utf8(buffer) + .map_err(Error::ProxyRequestRefused)?, + )), + b'9' => Ok(Self::BadRequest( + BadRequest::from_utf8(buffer).map_err(Error::BadRequest)?, + )), + 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::NotFound { .. } => NOT_FOUND, - Self::Gone { .. } => GONE, - Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED, - Self::BadRequest { .. } => BAD_REQUEST, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::NotFound { message } => message, - Self::Gone { message } => message, - Self::ProxyRequestRefused { message } => message, - Self::BadRequest { message } => message, + Self::Default(default) => default.message(), + Self::NotFound(not_found) => not_found.message(), + Self::Gone(gone) => gone.message(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.message(), + Self::BadRequest(bad_request) => bad_request.message(), } - .as_deref() } -} -impl std::fmt::Display for Permanent { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Default { .. } => DEFAULT, - Self::NotFound { .. } => NOT_FOUND, - Self::Gone { .. } => GONE, - Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED, - Self::BadRequest { .. } => BAD_REQUEST, + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Default(default) => default.message_or_default(), + Self::NotFound(not_found) => not_found.message_or_default(), + Self::Gone(gone) => gone.message_or_default(), + Self::ProxyRequestRefused(proxy_request_refused) => { + proxy_request_refused.message_or_default() } - .1 - ) + Self::BadRequest(bad_request) => bad_request.message_or_default(), + } } -} -impl std::str::FromStr for Permanent { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("50") { - return Ok(Self::Default { - message: message(postfix), - }); + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::NotFound(not_found) => not_found.as_str(), + Self::Gone(gone) => gone.as_str(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_str(), + Self::BadRequest(bad_request) => bad_request.as_str(), } - if let Some(postfix) = header.strip_prefix("51") { - return Ok(Self::NotFound { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("52") { - return Ok(Self::Gone { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("53") { - return Ok(Self::ProxyRequestRefused { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("59") { - return Ok(Self::BadRequest { - 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()) + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::NotFound(not_found) => not_found.as_bytes(), + Self::Gone(gone) => gone.as_bytes(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_bytes(), + Self::BadRequest(bad_request) => bad_request.as_bytes(), + } } } #[test] -fn test_from_str() { - use std::str::FromStr; - +fn test() { + fn t(source: &str, message: Option<&str>) { + let b = source.as_bytes(); + let i = Permanent::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } // 50 - let default = Permanent::from_str("50 Message\r\n").unwrap(); - assert_eq!(default.message(), Some("Message")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Permanent::from_str("50\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - + t("50 Message\r\n", Some("Message")); + t("50\r\n", None); // 51 - let not_found = Permanent::from_str("51 Message\r\n").unwrap(); - assert_eq!(not_found.message(), Some("Message")); - assert_eq!(not_found.to_code(), NOT_FOUND.0); - assert_eq!(not_found.to_string(), NOT_FOUND.1); - - let not_found = Permanent::from_str("51\r\n").unwrap(); - assert_eq!(not_found.message(), None); - assert_eq!(not_found.to_code(), NOT_FOUND.0); - assert_eq!(not_found.to_string(), NOT_FOUND.1); - + t("51 Message\r\n", Some("Message")); + t("51\r\n", None); // 52 - let gone = Permanent::from_str("52 Message\r\n").unwrap(); - assert_eq!(gone.message(), Some("Message")); - assert_eq!(gone.to_code(), GONE.0); - assert_eq!(gone.to_string(), GONE.1); - - let gone = Permanent::from_str("52\r\n").unwrap(); - assert_eq!(gone.message(), None); - assert_eq!(gone.to_code(), GONE.0); - assert_eq!(gone.to_string(), GONE.1); - + t("52 Message\r\n", Some("Message")); + t("52\r\n", None); // 53 - let proxy_request_refused = Permanent::from_str("53 Message\r\n").unwrap(); - assert_eq!(proxy_request_refused.message(), Some("Message")); - assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0); - assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); - - let proxy_request_refused = Permanent::from_str("53\r\n").unwrap(); - assert_eq!(proxy_request_refused.message(), None); - assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0); - assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); - + t("53 Message\r\n", Some("Message")); + t("53\r\n", None); // 59 - let bad_request = Permanent::from_str("59 Message\r\n").unwrap(); - assert_eq!(bad_request.message(), Some("Message")); - assert_eq!(bad_request.to_code(), BAD_REQUEST.0); - assert_eq!(bad_request.to_string(), BAD_REQUEST.1); - - let bad_request = Permanent::from_str("59\r\n").unwrap(); - assert_eq!(bad_request.message(), None); - assert_eq!(bad_request.to_code(), BAD_REQUEST.0); - assert_eq!(bad_request.to_string(), BAD_REQUEST.1); + t("59 Message\r\n", Some("Message")); + t("59\r\n", None); } diff --git a/src/client/connection/response/failure/permanent/bad_request.rs b/src/client/connection/response/failure/permanent/bad_request.rs new file mode 100644 index 0000000..8cfa6f9 --- /dev/null +++ b/src/client/connection/response/failure/permanent/bad_request.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code +pub const CODE: &[u8] = b"59"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Bad request"; + +/// Hold header `String` for [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct BadRequest(String); + +impl BadRequest { + // 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()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let br = BadRequest::from_utf8("59 Message\r\n".as_bytes()).unwrap(); + assert_eq!(br.message(), Some("Message")); + assert_eq!(br.message_or_default(), "Message"); + assert_eq!(br.as_str(), "59 Message\r\n"); + assert_eq!(br.as_bytes(), "59 Message\r\n".as_bytes()); + + let br = BadRequest::from_utf8("59\r\n".as_bytes()).unwrap(); + assert_eq!(br.message(), None); + assert_eq!(br.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(br.as_str(), "59\r\n"); + assert_eq!(br.as_bytes(), "59\r\n".as_bytes()); + + // err + assert!(BadRequest::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(BadRequest::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(BadRequest::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/bad_request/error.rs b/src/client/connection/response/failure/permanent/bad_request/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/bad_request/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/failure/permanent/default.rs b/src/client/connection/response/failure/permanent/default.rs new file mode 100644 index 0000000..466333d --- /dev/null +++ b/src/client/connection/response/failure/permanent/default.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code +pub const CODE: &[u8] = b"50"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Permanent error"; + +/// Hold header `String` for [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) 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()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let d = Default::from_utf8("50 Message\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Message")); + assert_eq!(d.message_or_default(), "Message"); + assert_eq!(d.as_str(), "50 Message\r\n"); + assert_eq!(d.as_bytes(), "50 Message\r\n".as_bytes()); + + let d = Default::from_utf8("50\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "50\r\n"); + assert_eq!(d.as_bytes(), "50\r\n".as_bytes()); + + // err + assert!(Default::from_utf8("13 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/failure/permanent/default/error.rs b/src/client/connection/response/failure/permanent/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/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/failure/permanent/error.rs b/src/client/connection/response/failure/permanent/error.rs index 5cf1cf6..df334f5 100644 --- a/src/client/connection/response/failure/permanent/error.rs +++ b/src/client/connection/response/failure/permanent/error.rs @@ -1,22 +1,47 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + BadRequest(super::bad_request::Error), + Default(super::default::Error), + FirstByte(u8), + Gone(super::gone::Error), + NotFound(super::not_found::Error), + ProxyRequestRefused(super::proxy_request_refused::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::BadRequest(e) => { + write!(f, "BadRequest parse error: {e}") } - 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::Gone(e) => { + write!(f, "Gone parse error: {e}") + } + Self::NotFound(e) => { + write!(f, "NotFound parse error: {e}") + } + Self::ProxyRequestRefused(e) => { + write!(f, "ProxyRequestRefused 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/failure/permanent/gone.rs b/src/client/connection/response/failure/permanent/gone.rs new file mode 100644 index 0000000..f93d068 --- /dev/null +++ b/src/client/connection/response/failure/permanent/gone.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code +pub const CODE: &[u8] = b"52"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Resource gone"; + +/// Hold header `String` for [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Gone(String); + +impl Gone { + // 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()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let g = Gone::from_utf8("52 Message\r\n".as_bytes()).unwrap(); + assert_eq!(g.message(), Some("Message")); + assert_eq!(g.message_or_default(), "Message"); + assert_eq!(g.as_str(), "52 Message\r\n"); + assert_eq!(g.as_bytes(), "52 Message\r\n".as_bytes()); + + let g = Gone::from_utf8("52\r\n".as_bytes()).unwrap(); + assert_eq!(g.message(), None); + assert_eq!(g.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(g.as_str(), "52\r\n"); + assert_eq!(g.as_bytes(), "52\r\n".as_bytes()); + + // err + assert!(Gone::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Gone::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Gone::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/gone/error.rs b/src/client/connection/response/failure/permanent/gone/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/gone/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/failure/permanent/not_found.rs b/src/client/connection/response/failure/permanent/not_found.rs new file mode 100644 index 0000000..d5ddca9 --- /dev/null +++ b/src/client/connection/response/failure/permanent/not_found.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code +pub const CODE: &[u8] = b"51"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Not Found"; + +/// Hold header `String` for [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotFound(String); + +impl NotFound { + // 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()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let nf = NotFound::from_utf8("51 Message\r\n".as_bytes()).unwrap(); + assert_eq!(nf.message(), Some("Message")); + assert_eq!(nf.message_or_default(), "Message"); + assert_eq!(nf.as_str(), "51 Message\r\n"); + assert_eq!(nf.as_bytes(), "51 Message\r\n".as_bytes()); + + let nf = NotFound::from_utf8("51\r\n".as_bytes()).unwrap(); + assert_eq!(nf.message(), None); + assert_eq!(nf.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(nf.as_str(), "51\r\n"); + assert_eq!(nf.as_bytes(), "51\r\n".as_bytes()); + + // err + assert!(NotFound::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(NotFound::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotFound::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/not_found/error.rs b/src/client/connection/response/failure/permanent/not_found/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/not_found/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/failure/permanent/proxy_request_refused.rs b/src/client/connection/response/failure/permanent/proxy_request_refused.rs new file mode 100644 index 0000000..fba229c --- /dev/null +++ b/src/client/connection/response/failure/permanent/proxy_request_refused.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code +pub const CODE: &[u8] = b"53"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Proxy request refused"; + +/// Hold header `String` for [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ProxyRequestRefused(String); + +impl ProxyRequestRefused { + // 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()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let prf = ProxyRequestRefused::from_utf8("53 Message\r\n".as_bytes()).unwrap(); + assert_eq!(prf.message(), Some("Message")); + assert_eq!(prf.message_or_default(), "Message"); + assert_eq!(prf.as_str(), "53 Message\r\n"); + assert_eq!(prf.as_bytes(), "53 Message\r\n".as_bytes()); + + let prf = ProxyRequestRefused::from_utf8("53\r\n".as_bytes()).unwrap(); + assert_eq!(prf.message(), None); + assert_eq!(prf.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(prf.as_str(), "53\r\n"); + assert_eq!(prf.as_bytes(), "53\r\n".as_bytes()); + + // err + assert!(ProxyRequestRefused::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ProxyRequestRefused::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ProxyRequestRefused::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs b/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/proxy_request_refused/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}") + } + } + } +}