diff --git a/Cargo.toml b/Cargo.toml index 52cb044..f54e4c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.14.1" +version = "0.15.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 3ede7d0..9f2b901 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -1,37 +1,126 @@ //! Read and parse Gemini response as Object -pub mod data; +pub mod certificate; +pub mod data; // @TODO deprecated pub mod error; -pub mod meta; +pub mod failure; +pub mod input; +pub mod redirect; +pub mod success; +pub use certificate::Certificate; pub use error::Error; -pub use meta::Meta; +pub use failure::Failure; +pub use input::Input; +pub use redirect::Redirect; +pub use success::Success; use super::Connection; -use gio::Cancellable; -use glib::Priority; +use gio::{Cancellable, IOStream}; +use glib::{object::IsA, Priority}; -pub struct Response { - pub connection: Connection, - pub meta: Meta, +const HEADER_LEN: usize = 0x400; // 1024 + +/// https://geminiprotocol.net/docs/protocol-specification.gmi#responses +pub enum Response { + Input(Input), // 1* + Success(Success), // 2* + Redirect(Redirect), // 3* + Failure(Failure), // 4*,5* + Certificate(Certificate), // 6* } impl Response { - // Constructors - - /// Create new `Self` from given `Connection` - /// * useful for manual [IOStream](https://docs.gtk.org/gio/class.IOStream.html) handle (based on `Meta` bytes pre-parsed) + /// Asynchronously create new `Self` for given `Connection` pub fn from_connection_async( connection: Connection, priority: Priority, cancellable: Cancellable, callback: impl FnOnce(Result) + 'static, ) { - Meta::from_stream_async(connection.stream(), priority, cancellable, |result| { - callback(match result { - Ok(meta) => Ok(Self { connection, meta }), - Err(e) => Err(Error::Meta(e)), - }) - }) + from_stream_async( + Vec::with_capacity(HEADER_LEN), + connection.stream(), + cancellable, + priority, + |result| { + callback(match result { + Ok(buffer) => match buffer.first() { + Some(byte) => match byte { + 1 => match Input::from_utf8(&buffer) { + Ok(input) => Ok(Self::Input(input)), + Err(e) => Err(Error::Input(e)), + }, + 2 => match Success::from_utf8(&buffer) { + Ok(success) => Ok(Self::Success(success)), + Err(e) => Err(Error::Success(e)), + }, + 3 => match Redirect::from_utf8(&buffer) { + Ok(redirect) => Ok(Self::Redirect(redirect)), + Err(e) => Err(Error::Redirect(e)), + }, + 4 | 5 => match Failure::from_utf8(&buffer) { + Ok(failure) => Ok(Self::Failure(failure)), + Err(e) => Err(Error::Failure(e)), + }, + 6 => match Certificate::from_utf8(&buffer) { + Ok(certificate) => Ok(Self::Certificate(certificate)), + Err(e) => Err(Error::Certificate(e)), + }, + b => Err(Error::Code(*b)), + }, + None => Err(Error::Protocol), + }, + Err(e) => Err(e), + }) + }, + ); } } + +// Tools + +/// Asynchronously read header bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// +/// Return UTF-8 buffer collected +/// * requires `IOStream` reference to keep `Connection` active in async thread +fn from_stream_async( + mut buffer: Vec, + stream: impl IsA, + cancellable: Cancellable, + priority: Priority, + on_complete: impl FnOnce(Result, Error>) + 'static, +) { + use gio::prelude::{IOStreamExt, InputStreamExtManual}; + + stream.input_stream().read_async( + vec![0], + priority, + Some(&cancellable.clone()), + move |result| match result { + Ok((mut bytes, size)) => { + // Expect valid header length + if size == 0 || buffer.len() >= HEADER_LEN { + return on_complete(Err(Error::Protocol)); + } + + // Read next byte without record + if bytes.contains(&b'\r') { + return from_stream_async(buffer, stream, cancellable, priority, on_complete); + } + + // Complete without record + if bytes.contains(&b'\n') { + return on_complete(Ok(buffer)); + } + + // Record + buffer.append(&mut bytes); + + // Continue + from_stream_async(buffer, stream, cancellable, priority, on_complete); + } + Err((data, e)) => on_complete(Err(Error::Stream(e, data))), + }, + ) +} diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs new file mode 100644 index 0000000..450d6d7 --- /dev/null +++ b/src/client/connection/response/certificate.rs @@ -0,0 +1,113 @@ +pub mod error; +pub use error::Error; + +const REQUIRED: (u8, &str) = (10, "Certificate required"); +const NOT_AUTHORIZED: (u8, &str) = (11, "Certificate not authorized"); +const NOT_VALID: (u8, &str) = (11, "Certificate not valid"); + +/// 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 }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized + NotAuthorized { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid + NotValid { message: Option }, +} + +impl Certificate { + // Constructors + + /// 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)), + } + } + + // 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, + } + .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 { message } => message.as_deref().unwrap_or(REQUIRED.1), + Self::NotAuthorized { message } => message.as_deref().unwrap_or(NOT_AUTHORIZED.1), + Self::NotValid { message } => message.as_deref().unwrap_or(NOT_VALID.1), + } + ) + } +} + +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), + }); + } + 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); + + 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); +} diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/certificate/error.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Code, + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Status code error") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + } + } +} diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index 9834190..99895d9 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -1,19 +1,50 @@ -use std::fmt::{Display, Formatter, Result}; +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; #[derive(Debug)] pub enum Error { - Meta(super::meta::Error), - Stream, + Certificate(super::certificate::Error), + Code(u8), + Failure(super::failure::Error), + Input(super::input::Error), + Protocol, + Redirect(super::redirect::Error), + Stream(glib::Error, Vec), + Success(super::success::Error), + Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Meta(e) => { - write!(f, "Meta read error: {e}") + Self::Certificate(e) => { + write!(f, "Certificate error: {e}") } - Self::Stream => { - write!(f, "I/O stream error") + Self::Code(e) => { + write!(f, "Code group error: {e}*") + } + Self::Failure(e) => { + write!(f, "Failure error: {e}") + } + Self::Input(e) => { + write!(f, "Input error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Redirect(e) => { + write!(f, "Redirect error: {e}") + } + Self::Stream(e, ..) => { + write!(f, "I/O stream error: {e}") + } + Self::Success(e) => { + write!(f, "Success error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") } } } diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs new file mode 100644 index 0000000..c0e76ec --- /dev/null +++ b/src/client/connection/response/failure.rs @@ -0,0 +1,54 @@ +pub mod error; +pub mod permanent; +pub mod temporary; + +pub use error::Error; +pub use permanent::Permanent; +pub use temporary::Temporary; + +pub enum Failure { + /// 4* status code group + /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure + Temporary(Temporary), + /// 5* status code group + /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure + Permanent(Permanent), +} + +impl Failure { + // Constructors + + /// Create new `Self` from buffer include header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + match buffer.first() { + Some(byte) => match byte { + 4 => match Temporary::from_utf8(buffer) { + Ok(input) => Ok(Self::Temporary(input)), + Err(e) => Err(Error::Temporary(e)), + }, + 5 => match Permanent::from_utf8(buffer) { + Ok(failure) => Ok(Self::Permanent(failure)), + Err(e) => Err(Error::Permanent(e)), + }, + b => Err(Error::Code(*b)), + }, + None => Err(Error::Protocol), + } + } + + // 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(), + Self::Temporary(temporary) => temporary.message(), + } + } +} diff --git a/src/client/connection/response/failure/error.rs b/src/client/connection/response/failure/error.rs new file mode 100644 index 0000000..60724eb --- /dev/null +++ b/src/client/connection/response/failure/error.rs @@ -0,0 +1,28 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code(u8), + Permanent(super::permanent::Error), + Protocol, + Temporary(super::temporary::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code(e) => { + write!(f, "Code group error: {e}*") + } + Self::Permanent(e) => { + write!(f, "Permanent failure group error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Temporary(e) => { + write!(f, "Temporary failure group error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs new file mode 100644 index 0000000..08b6c56 --- /dev/null +++ b/src/client/connection/response/failure/permanent.rs @@ -0,0 +1,169 @@ +pub mod error; +pub use error::Error; + +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"); + +/// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure +pub enum Permanent { + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50 + Default { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found + NotFound { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone + Gone { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused + ProxyRequestRefused { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request + BadRequest { message: Option }, +} + +impl Permanent { + // Constructors + + /// 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)), + } + } + + // 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, + } + .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 { message } => message.as_deref().unwrap_or(DEFAULT.1), + Self::NotFound { message } => message.as_deref().unwrap_or(NOT_FOUND.1), + Self::Gone { message } => message.as_deref().unwrap_or(GONE.1), + Self::ProxyRequestRefused { message } => + message.as_deref().unwrap_or(PROXY_REQUEST_REFUSED.1), + Self::BadRequest { message } => message.as_deref().unwrap_or(BAD_REQUEST.1), + } + ) + } +} + +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), + }); + } + 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()) + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + // 50 + let default = Permanent::from_str("50 Message\r\n").unwrap(); + assert_eq!(default.message(), Some("Message")); + assert_eq!(default.to_code(), DEFAULT.0); + + let default = Permanent::from_str("50\r\n").unwrap(); + assert_eq!(default.message(), None); + assert_eq!(default.to_code(), DEFAULT.0); + + // 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); + + 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); + + // 52 + let gone = Permanent::from_str("52 Message\r\n").unwrap(); + assert_eq!(gone.message(), Some("Message")); + assert_eq!(gone.to_code(), GONE.0); + + let gone = Permanent::from_str("52\r\n").unwrap(); + assert_eq!(gone.message(), None); + assert_eq!(gone.to_code(), GONE.0); + + // 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); + + 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); + + // 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); + + 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); +} diff --git a/src/client/connection/response/failure/permanent/error.rs b/src/client/connection/response/failure/permanent/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/failure/permanent/error.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Code, + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Status code error") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs new file mode 100644 index 0000000..363645e --- /dev/null +++ b/src/client/connection/response/failure/temporary.rs @@ -0,0 +1,169 @@ +pub mod error; +pub use error::Error; + +const DEFAULT: (u8, &str) = (40, "Unspecified"); +const SERVER_UNAVAILABLE: (u8, &str) = (41, "Server unavailable"); +const CGI_ERROR: (u8, &str) = (42, "CGI error"); +const PROXY_ERROR: (u8, &str) = (43, "Proxy error"); +const SLOW_DOWN: (u8, &str) = (44, "Slow down"); + +/// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure +pub enum Temporary { + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40 + Default { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable + ServerUnavailable { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error + CgiError { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error + ProxyError { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down + SlowDown { message: Option }, +} + +impl Temporary { + // Constructors + + /// 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)), + } + } + + // Getters + + pub fn to_code(&self) -> u8 { + match self { + Self::Default { .. } => DEFAULT, + Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, + Self::CgiError { .. } => CGI_ERROR, + Self::ProxyError { .. } => PROXY_ERROR, + Self::SlowDown { .. } => SLOW_DOWN, + } + .0 + } + + pub fn message(&self) -> Option<&str> { + match self { + Self::Default { message } => message, + Self::ServerUnavailable { message } => message, + Self::CgiError { message } => message, + Self::ProxyError { message } => message, + Self::SlowDown { message } => message, + } + .as_deref() + } +} + +impl std::fmt::Display for Temporary { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Default { message } => message.as_deref().unwrap_or(DEFAULT.1), + Self::ServerUnavailable { message } => + message.as_deref().unwrap_or(SERVER_UNAVAILABLE.1), + Self::CgiError { message } => message.as_deref().unwrap_or(CGI_ERROR.1), + Self::ProxyError { message } => message.as_deref().unwrap_or(PROXY_ERROR.1), + Self::SlowDown { message } => message.as_deref().unwrap_or(SLOW_DOWN.1), + } + ) + } +} + +impl std::str::FromStr for Temporary { + type Err = Error; + fn from_str(header: &str) -> Result { + if let Some(postfix) = header.strip_prefix("40") { + return Ok(Self::Default { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("41") { + return Ok(Self::ServerUnavailable { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("42") { + return Ok(Self::CgiError { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("43") { + return Ok(Self::ProxyError { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("44") { + return Ok(Self::SlowDown { + 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; + + // 40 + let default = Temporary::from_str("40 Message\r\n").unwrap(); + assert_eq!(default.message(), Some("Message")); + assert_eq!(default.to_code(), DEFAULT.0); + + let default = Temporary::from_str("40\r\n").unwrap(); + assert_eq!(default.message(), None); + assert_eq!(default.to_code(), DEFAULT.0); + + // 41 + let server_unavailable = Temporary::from_str("41 Message\r\n").unwrap(); + assert_eq!(server_unavailable.message(), Some("Message")); + assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); + + let server_unavailable = Temporary::from_str("41\r\n").unwrap(); + assert_eq!(server_unavailable.message(), None); + assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); + + // 42 + let cgi_error = Temporary::from_str("42 Message\r\n").unwrap(); + assert_eq!(cgi_error.message(), Some("Message")); + assert_eq!(cgi_error.to_code(), CGI_ERROR.0); + + let cgi_error = Temporary::from_str("42\r\n").unwrap(); + assert_eq!(cgi_error.message(), None); + assert_eq!(cgi_error.to_code(), CGI_ERROR.0); + + // 43 + let proxy_error = Temporary::from_str("43 Message\r\n").unwrap(); + assert_eq!(proxy_error.message(), Some("Message")); + assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); + + let proxy_error = Temporary::from_str("43\r\n").unwrap(); + assert_eq!(proxy_error.message(), None); + assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); + + // 44 + let slow_down = Temporary::from_str("44 Message\r\n").unwrap(); + assert_eq!(slow_down.message(), Some("Message")); + assert_eq!(slow_down.to_code(), SLOW_DOWN.0); + + let slow_down = Temporary::from_str("44\r\n").unwrap(); + assert_eq!(slow_down.message(), None); + assert_eq!(slow_down.to_code(), SLOW_DOWN.0); +} diff --git a/src/client/connection/response/failure/temporary/error.rs b/src/client/connection/response/failure/temporary/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/failure/temporary/error.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Code, + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Status code error") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + } + } +} diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs new file mode 100644 index 0000000..56dcc08 --- /dev/null +++ b/src/client/connection/response/input.rs @@ -0,0 +1,109 @@ +pub mod error; +pub use error::Error; + +const DEFAULT: (u8, &str) = (10, "Input"); +const SENSITIVE: (u8, &str) = (11, "Sensitive input"); + +pub enum Input { + Default { message: Option }, + Sensitive { message: Option }, +} + +impl Input { + // Constructors + + /// 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)), + } + } + + // 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, + } + .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 { message } => message.as_deref().unwrap_or(DEFAULT.1), + Self::Sensitive { message } => message.as_deref().unwrap_or(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), + }); + } + 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()) + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + // 10 + let default = Input::from_str("10 Default\r\n").unwrap(); + assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.message(), Some("Default")); + assert_eq!(default.to_string(), "Default"); + + let default = Input::from_str("10\r\n").unwrap(); + assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.message(), None); + assert_eq!(default.to_string(), DEFAULT.1); + + // 11 + let sensitive = Input::from_str("11 Sensitive\r\n").unwrap(); + assert_eq!(sensitive.to_code(), SENSITIVE.0); + assert_eq!(sensitive.message(), Some("Sensitive")); + assert_eq!(sensitive.to_string(), "Sensitive"); + + let sensitive = Input::from_str("11\r\n").unwrap(); + assert_eq!(sensitive.to_code(), SENSITIVE.0); + assert_eq!(sensitive.message(), None); + assert_eq!(sensitive.to_string(), SENSITIVE.1); +} diff --git a/src/client/connection/response/meta/data/error.rs b/src/client/connection/response/input/error.rs similarity index 59% rename from src/client/connection/response/meta/data/error.rs rename to src/client/connection/response/input/error.rs index 49455cd..ae589e8 100644 --- a/src/client/connection/response/meta/data/error.rs +++ b/src/client/connection/response/input/error.rs @@ -1,16 +1,19 @@ -use std::fmt::{Display, Formatter, Result}; +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; #[derive(Debug)] pub enum Error { - Decode(std::string::FromUtf8Error), Protocol, + Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Decode(e) => { - write!(f, "Decode error: {e}") + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") } Self::Protocol => { write!(f, "Protocol error") diff --git a/src/client/connection/response/meta.rs b/src/client/connection/response/meta.rs deleted file mode 100644 index 26388ad..0000000 --- a/src/client/connection/response/meta.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Components for reading and parsing meta bytes from response: -//! * [Gemini status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) -//! * meta data (for interactive statuses like 10, 11, 30 etc) -//! * MIME type - -pub mod data; -pub mod error; -pub mod mime; -pub mod status; - -pub use data::Data; -pub use error::Error; -pub use mime::Mime; -pub use status::Status; - -use gio::{ - prelude::{IOStreamExt, InputStreamExtManual}, - Cancellable, IOStream, -}; -use glib::{object::IsA, Priority}; - -pub const MAX_LEN: usize = 0x400; // 1024 - -pub struct Meta { - pub status: Status, - pub data: Option, - pub mime: Option, - // @TODO - // charset: Option, - // language: Option, -} - -impl Meta { - // Constructors - - /// Create new `Self` from UTF-8 buffer - /// * supports entire response or just meta slice - pub fn from_utf8(buffer: &[u8]) -> Result { - // Calculate buffer length once - let len = buffer.len(); - - // Parse meta bytes only - match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { - Some(slice) => { - // Parse data - let data = Data::from_utf8(slice); - - if let Err(e) = data { - return Err(Error::Data(e)); - } - - // MIME - - let mime = Mime::from_utf8(slice); - - if let Err(e) = mime { - return Err(Error::Mime(e)); - } - - // Status - - let status = Status::from_utf8(slice); - - if let Err(e) = status { - return Err(Error::Status(e)); - } - - Ok(Self { - data: data.unwrap(), - mime: mime.unwrap(), - status: status.unwrap(), - }) - } - None => Err(Error::Protocol), - } - } - - /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) - pub fn from_stream_async( - stream: impl IsA, - priority: Priority, - cancellable: Cancellable, - on_complete: impl FnOnce(Result) + 'static, - ) { - read_from_stream_async( - Vec::with_capacity(MAX_LEN), - stream, - cancellable, - priority, - |result| match result { - Ok(buffer) => on_complete(Self::from_utf8(&buffer)), - Err(e) => on_complete(Err(e)), - }, - ); - } -} - -// Tools - -/// Asynchronously read all meta bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) -/// -/// Return UTF-8 buffer collected -/// * require `IOStream` reference to keep `Connection` active in async thread -pub fn read_from_stream_async( - mut buffer: Vec, - stream: impl IsA, - cancellable: Cancellable, - priority: Priority, - on_complete: impl FnOnce(Result, Error>) + 'static, -) { - stream.input_stream().read_async( - vec![0], - priority, - Some(&cancellable.clone()), - move |result| match result { - Ok((mut bytes, size)) => { - // Expect valid header length - if size == 0 || buffer.len() >= MAX_LEN { - return on_complete(Err(Error::Protocol)); - } - - // Read next byte without record - if bytes.contains(&b'\r') { - return read_from_stream_async( - buffer, - stream, - cancellable, - priority, - on_complete, - ); - } - - // Complete without record - if bytes.contains(&b'\n') { - return on_complete(Ok(buffer)); - } - - // Record - buffer.append(&mut bytes); - - // Continue - read_from_stream_async(buffer, stream, cancellable, priority, on_complete); - } - Err((data, e)) => on_complete(Err(Error::InputStream(data, e))), - }, - ); -} diff --git a/src/client/connection/response/meta/charset.rs b/src/client/connection/response/meta/charset.rs deleted file mode 100644 index 1673a59..0000000 --- a/src/client/connection/response/meta/charset.rs +++ /dev/null @@ -1 +0,0 @@ -// @TODO diff --git a/src/client/connection/response/meta/data.rs b/src/client/connection/response/meta/data.rs deleted file mode 100644 index d5eef6b..0000000 --- a/src/client/connection/response/meta/data.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Components for reading and parsing meta **data** bytes from response -//! (e.g. placeholder text for 10, 11, url string for 30, 31 etc) - -pub mod error; -pub use error::Error; - -use glib::GString; - -/// Meta **data** holder -/// -/// For example, `value` could contain: -/// * placeholder text for 10, 11 status -/// * URL string for 30, 31 status -pub struct Data(GString); - -impl Data { - // Constructors - - /// Parse meta **data** from UTF-8 buffer - /// from entire response or just header slice - /// - /// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) - /// that does not expect any data in header - pub fn from_utf8(buffer: &[u8]) -> Result, Error> { - // Define max buffer length for this method - const MAX_LEN: usize = 0x400; // 1024 - - // Init bytes buffer - let mut bytes: Vec = Vec::with_capacity(MAX_LEN); - - // Calculate len once - let len = buffer.len(); - - // Skip 3 bytes for status code of `MAX_LEN` expected - match buffer.get(3..if len > MAX_LEN { MAX_LEN - 3 } else { len }) { - Some(slice) => { - for &byte in slice { - // End of header - if byte == b'\r' { - break; - } - - // Continue - bytes.push(byte); - } - - // Assumes the bytes are valid UTF-8 - match GString::from_utf8(bytes) { - Ok(data) => Ok(match data.is_empty() { - false => Some(Self(data)), - true => None, - }), - Err(e) => Err(Error::Decode(e)), - } - } - None => Err(Error::Protocol), - } - } - - // Getters - - /// Get `Self` as `std::str` - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - /// Get `Self` as `glib::GString` - pub fn as_gstring(&self) -> &GString { - &self.0 - } - - /// Get `glib::GString` copy of `Self` - pub fn to_gstring(&self) -> GString { - self.0.clone() - } -} - -impl std::fmt::Display for Data { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} diff --git a/src/client/connection/response/meta/error.rs b/src/client/connection/response/meta/error.rs deleted file mode 100644 index 55abb24..0000000 --- a/src/client/connection/response/meta/error.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Data(super::data::Error), - InputStream(Vec, glib::Error), - Mime(super::mime::Error), - Protocol, - Status(super::status::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::Data(e) => { - write!(f, "Data error: {e}") - } - Self::InputStream(_, e) => { - // @TODO - write!(f, "Input stream error: {e}") - } - Self::Mime(e) => { - write!(f, "MIME error: {e}") - } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Status(e) => { - write!(f, "Status error: {e}") - } - } - } -} diff --git a/src/client/connection/response/meta/language.rs b/src/client/connection/response/meta/language.rs deleted file mode 100644 index 1673a59..0000000 --- a/src/client/connection/response/meta/language.rs +++ /dev/null @@ -1 +0,0 @@ -// @TODO diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs deleted file mode 100644 index 12b6810..0000000 --- a/src/client/connection/response/meta/mime.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! MIME type parser for different data types - -pub mod error; -pub use error::Error; - -use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; - -/// MIME type holder for `Response` (by [Gemtext specification](https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters)) -/// * the value stored in lowercase -pub struct Mime(String); - -impl Mime { - // Constructors - - /// Create new `Self` from UTF-8 buffer (that includes **header**) - /// * return `None` for non 2* [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) - pub fn from_utf8(buffer: &[u8]) -> Result, Error> { - // Define max buffer length for this method - const MAX_LEN: usize = 0x400; // 1024 - - // Calculate buffer length once - let len = buffer.len(); - - // Parse meta bytes only - match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { - Some(b) => match std::str::from_utf8(b) { - Ok(s) => Self::from_string(s), - Err(e) => Err(Error::Decode(e)), - }, - None => Err(Error::Protocol), - } - } - - /// Create new `Self` from `str::str` that includes **header** - /// * return `None` for non 2* [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) - pub fn from_string(s: &str) -> Result, Error> { - if !s.starts_with("2") { - return Ok(None); - } - match parse(s) { - Some(v) => Ok(Some(Self(v))), - None => Err(Error::Undefined), - } - } - - // Getters - - /// Get `Self` as lowercase `std::str` - pub fn as_str(&self) -> &str { - self.0.as_str() - } -} - -impl std::fmt::Display for Mime { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Extract MIME type from from string that includes **header** -pub fn parse(s: &str) -> Option { - Regex::split_simple( - r"^2\d{1}\s([^\/]+\/[^\s;]+)", - s, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ) - .get(1) - .map(|this| this.to_lowercase()) -} diff --git a/src/client/connection/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs deleted file mode 100644 index 5b68dbc..0000000 --- a/src/client/connection/response/meta/mime/error.rs +++ /dev/null @@ -1,22 +0,0 @@ -#[derive(Debug)] -pub enum Error { - Decode(std::str::Utf8Error), - Protocol, - Undefined, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Decode(e) => { - write!(f, "Decode error: {e}") - } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Undefined => { - write!(f, "MIME type undefined") - } - } - } -} diff --git a/src/client/connection/response/meta/status.rs b/src/client/connection/response/meta/status.rs deleted file mode 100644 index 0cd6861..0000000 --- a/src/client/connection/response/meta/status.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Parser and holder tools for -//! [Status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) - -pub mod error; -pub use error::Error; - -/// Holder for [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) -#[derive(Debug)] -pub enum Status { - // Input - Input = 10, - SensitiveInput = 11, - // Success - Success = 20, - // Redirect - Redirect = 30, - PermanentRedirect = 31, - // Temporary failure - TemporaryFailure = 40, - ServerUnavailable = 41, - CgiError = 42, - ProxyError = 43, - SlowDown = 44, - // Permanent failure - PermanentFailure = 50, - NotFound = 51, - ResourceGone = 52, - ProxyRequestRefused = 53, - BadRequest = 59, - // Client certificates - CertificateRequest = 60, - CertificateUnauthorized = 61, - CertificateInvalid = 62, -} - -impl std::fmt::Display for Status { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Status::Input => "Input", - Status::SensitiveInput => "Sensitive Input", - Status::Success => "Success", - Status::Redirect => "Redirect", - Status::PermanentRedirect => "Permanent Redirect", - Status::TemporaryFailure => "Temporary Failure", - Status::ServerUnavailable => "Server Unavailable", - Status::CgiError => "CGI Error", - Status::ProxyError => "Proxy Error", - Status::SlowDown => "Slow Down", - Status::PermanentFailure => "Permanent Failure", - Status::NotFound => "Not Found", - Status::ResourceGone => "Resource Gone", - Status::ProxyRequestRefused => "Proxy Request Refused", - Status::BadRequest => "Bad Request", - Status::CertificateRequest => "Certificate Request", - Status::CertificateUnauthorized => "Certificate Unauthorized", - Status::CertificateInvalid => "Certificate Invalid", - } - ) - } -} - -impl Status { - /// Create new `Self` from UTF-8 buffer - /// - /// * includes `Self::from_string` parser, it means that given buffer should contain some **header** - pub fn from_utf8(buffer: &[u8]) -> Result { - match buffer.get(0..2) { - Some(b) => match std::str::from_utf8(b) { - Ok(s) => Self::from_string(s), - Err(e) => Err(Error::Decode(e)), - }, - None => Err(Error::Protocol), - } - } - - /// Create new `Self` from string that includes **header** - pub fn from_string(code: &str) -> Result { - match code { - // Input - "10" => Ok(Self::Input), - "11" => Ok(Self::SensitiveInput), - // Success - "20" => Ok(Self::Success), - // Redirect - "30" => Ok(Self::Redirect), - "31" => Ok(Self::PermanentRedirect), - // Temporary failure - "40" => Ok(Self::TemporaryFailure), - "41" => Ok(Self::ServerUnavailable), - "42" => Ok(Self::CgiError), - "43" => Ok(Self::ProxyError), - "44" => Ok(Self::SlowDown), - // Permanent failure - "50" => Ok(Self::PermanentFailure), - "51" => Ok(Self::NotFound), - "52" => Ok(Self::ResourceGone), - "53" => Ok(Self::ProxyRequestRefused), - "59" => Ok(Self::BadRequest), - // Client certificates - "60" => Ok(Self::CertificateRequest), - "61" => Ok(Self::CertificateUnauthorized), - "62" => Ok(Self::CertificateInvalid), - _ => Err(Error::Undefined), - } - } -} diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs new file mode 100644 index 0000000..cfb8c21 --- /dev/null +++ b/src/client/connection/response/redirect.rs @@ -0,0 +1,112 @@ +pub mod error; +pub use error::Error; + +use glib::GStringPtr; + +const TEMPORARY: u8 = 30; +const PERMANENT: u8 = 31; + +pub enum Redirect { + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection + Temporary { target: String }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection + Permanent { target: String }, +} + +impl Redirect { + // Constructors + + /// 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)), + } + } + + // Convertors + + pub fn to_code(&self) -> u8 { + match self { + Self::Permanent { .. } => PERMANENT, + Self::Temporary { .. } => TEMPORARY, + } + } + + // Getters + + pub fn target(&self) -> &str { + match self { + Self::Permanent { target } => target, + Self::Temporary { target } => target, + } + } +} + +impl std::fmt::Display for Redirect { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Permanent { target } => format!("Permanent redirection to `{target}`"), + Self::Temporary { target } => format!("Temporary redirection to `{target}`"), + } + ) + } +} + +impl std::str::FromStr for Redirect { + type Err = Error; + fn from_str(header: &str) -> Result { + use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; + + let regex = Regex::split_simple( + r"^3(\d)\s([^\r\n]+)", + header, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ); + + match regex.get(1) { + Some(code) => match code.as_str() { + "0" => Ok(Self::Temporary { + target: target(regex.get(2))?, + }), + "1" => Ok(Self::Permanent { + target: target(regex.get(2))?, + }), + _ => todo!(), + }, + None => Err(Error::Protocol), + } + } +} + +fn target(value: Option<&GStringPtr>) -> Result { + match value { + Some(target) => { + let target = target.trim(); + if target.is_empty() { + Err(Error::Target) + } else { + Ok(target.to_string()) + } + } + None => Err(Error::Target), + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + let temporary = Redirect::from_str("30 /uri\r\n").unwrap(); + assert_eq!(temporary.target(), "/uri"); + assert_eq!(temporary.to_code(), TEMPORARY); + + let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); + assert_eq!(permanent.target(), "/uri"); + assert_eq!(permanent.to_code(), PERMANENT); +} diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs new file mode 100644 index 0000000..b3d3579 --- /dev/null +++ b/src/client/connection/response/redirect/error.rs @@ -0,0 +1,27 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Protocol, + Target, + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Target => { + write!(f, "Target error") + } + } + } +} diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs new file mode 100644 index 0000000..e5ad6f4 --- /dev/null +++ b/src/client/connection/response/success.rs @@ -0,0 +1,89 @@ +pub mod error; +pub use error::Error; + +const DEFAULT: (u8, &str) = (20, "Success"); + +pub enum Success { + Default { mime: String }, + // reserved for 2* codes +} + +impl Success { + // Constructors + + /// 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)), + } + } + + // Convertors + + pub fn to_code(&self) -> u8 { + match self { + Self::Default { .. } => DEFAULT.0, + } + } + + // Getters + + pub fn mime(&self) -> &str { + match self { + Self::Default { mime } => mime, + } + } +} + +impl std::fmt::Display for Success { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Default { .. } => DEFAULT.1, + } + ) + } +} + +impl std::str::FromStr for Success { + type Err = Error; + fn from_str(header: &str) -> Result { + use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; + + match Regex::split_simple( + r"^20\s([^\/]+\/[^\s;]+)", + header, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ) + .get(1) + { + Some(mime) => { + let mime = mime.trim(); + if mime.is_empty() { + Err(Error::Mime) + } else { + Ok(Self::Default { + mime: mime.to_lowercase(), + }) + } + } + None => Err(Error::Protocol), + } + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + let default = Success::from_str("20 text/gemini; charset=utf-8; lang=en\r\n").unwrap(); + + assert_eq!(default.mime(), "text/gemini"); + assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); +} diff --git a/src/client/connection/response/meta/status/error.rs b/src/client/connection/response/success/error.rs similarity index 52% rename from src/client/connection/response/meta/status/error.rs rename to src/client/connection/response/success/error.rs index 24090dd..2dbe363 100644 --- a/src/client/connection/response/meta/status/error.rs +++ b/src/client/connection/response/success/error.rs @@ -1,23 +1,26 @@ -use std::fmt::{Display, Formatter, Result}; +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; #[derive(Debug)] pub enum Error { - Decode(std::str::Utf8Error), Protocol, - Undefined, + Mime, + Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Decode(e) => { - write!(f, "Decode error: {e}") + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") } Self::Protocol => { write!(f, "Protocol error") } - Self::Undefined => { - write!(f, "Undefined") + Self::Mime => { + write!(f, "MIME error") } } }