diff --git a/Cargo.toml b/Cargo.toml index 39e2fd8..801fb2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ggemini" -version = "0.17.0" -edition = "2021" +version = "0.20.1" +edition = "2024" license = "MIT" readme = "README.md" description = "Glib/Gio-oriented network API for Gemini protocol" @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.20.4" +version = "0.21.0" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.20.4" +version = "0.21.0" features = ["v2_66"] diff --git a/README.md b/README.md index cd21d88..03c633e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ use gio::*; use glib::*; use ggemini::client::{ - connection::{Request, Response}, + connection::{request::{Mode, Request}, Response}, Client, }; @@ -51,13 +51,15 @@ fn main() -> ExitCode { Client::new().request_async( Request::Gemini { // or `Request::Titan` uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), + mode: Mode::HeaderOnly // handle content separately (based on MIME) }, Priority::DEFAULT, Cancellable::new(), - None, // optional `GTlsCertificate` + None, // optional auth `GTlsCertificate` + None, // optional TOFU `GTlsCertificate` array |result| match result { Ok((response, _connection)) => match response { - Response::Success(success) => match success.mime() { + Response::Success(success) => match success.mime().unwrap().as_str() { "text/gemini" => todo!(), _ => todo!(), }, diff --git a/src/client.rs b/src/client.rs index 342eac3..2152781 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ pub mod error; pub use connection::{Connection, Request, Response}; pub use error::Error; -use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate}; +use gio::{Cancellable, SocketClient, SocketProtocol, TlsCertificate, prelude::SocketClientExt}; use glib::Priority; // Defaults @@ -59,7 +59,8 @@ impl Client { request: Request, priority: Priority, cancellable: Cancellable, - certificate: Option, + client_certificate: Option, + server_certificates: Option>, callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static, ) { // Begin new connection @@ -73,30 +74,33 @@ impl Client { move |result| match result { Ok(socket_connection) => { match Connection::build( - socket_connection, - certificate, - Some(network_address), + socket_connection.clone(), + network_address, + client_certificate, + server_certificates, is_session_resumption, ) { - Ok(connection) => connection.request_async( + Ok(connection) => connection.clone().request_async( request, priority, cancellable, move |result| { callback(match result { Ok(response) => Ok(response), - Err(e) => Err(Error::Connection(e)), + Err(e) => Err(Error::Request(connection, e)), }) }, ), - Err(e) => callback(Err(Error::Connection(e))), + Err(e) => { + callback(Err(Error::Connection(socket_connection, e))) + } } } - Err(e) => callback(Err(Error::Connect(e))), + Err(e) => callback(Err(Error::Connect(network_address, e))), } }) } - Err(e) => callback(Err(Error::Request(e))), + Err(e) => callback(Err(Error::NetworkAddress(e))), } } diff --git a/src/client/connection.rs b/src/client/connection.rs index b7b2eb9..6be90f1 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -3,21 +3,22 @@ pub mod request; pub mod response; pub use error::Error; -pub use request::Request; +pub use request::{Mode, Request}; pub use response::Response; -// Local dependencies - use gio::{ - prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, + prelude::{IOStreamExt, OutputStreamExtManual, TlsCertificateExt, TlsConnectionExt}, }; use glib::{ - object::{Cast, ObjectExt}, Bytes, Priority, + object::{Cast, ObjectExt}, }; +#[derive(Debug, Clone)] pub struct Connection { + pub network_address: NetworkAddress, + pub socket_connection: SocketConnection, pub tls_client_connection: TlsClientConnection, } @@ -27,24 +28,28 @@ impl Connection { /// Create new `Self` pub fn build( socket_connection: SocketConnection, - certificate: Option, - server_identity: Option, + network_address: NetworkAddress, + client_certificate: Option, + server_certificates: Option>, is_session_resumption: bool, ) -> Result { Ok(Self { tls_client_connection: match new_tls_client_connection( &socket_connection, - server_identity.as_ref(), + Some(&network_address), + server_certificates, is_session_resumption, ) { Ok(tls_client_connection) => { - if let Some(ref certificate) = certificate { - tls_client_connection.set_certificate(certificate); + if let Some(ref c) = client_certificate { + tls_client_connection.set_certificate(c); } tls_client_connection } Err(e) => return Err(e), }, + network_address, + socket_connection, }) } @@ -70,36 +75,40 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match request { - Request::Gemini { .. } => Response::from_connection_async( - self, - priority, - cancellable, - |result, connection| { - callback(match result { - Ok(response) => Ok((response, connection)), - Err(e) => Err(Error::Response(e)), - }) - }, - ), + Request::Gemini { mode, .. } => match mode { + Mode::HeaderOnly => Response::header_from_connection_async( + self, + priority, + cancellable, + |result, connection| { + callback(match result { + Ok(response) => Ok((response, connection)), + Err(e) => Err(Error::Response(e)), + }) + }, + ), + }, // Make sure **all data bytes** sent to the destination // > A partial write is performed with the size of a message block, which is 16kB // > https://docs.openssl.org/3.0/man3/SSL_write/#notes - Request::Titan { data, .. } => output_stream.write_all_async( + Request::Titan { data, mode, .. } => output_stream.write_all_async( data, priority, Some(&cancellable.clone()), move |result| match result { - Ok(_) => Response::from_connection_async( - self, - priority, - cancellable, - |result, connection| { - callback(match result { - Ok(response) => Ok((response, connection)), - Err(e) => Err(Error::Response(e)), - }) - }, - ), + Ok(_) => match mode { + Mode::HeaderOnly => Response::header_from_connection_async( + self, + priority, + cancellable, + |result, connection| { + callback(match result { + Ok(response) => Ok((response, connection)), + Err(e) => Err(Error::Response(e)), + }) + }, + ), + }, Err((b, e)) => callback(Err(Error::Request(b, e))), }, ), @@ -120,14 +129,15 @@ impl Connection { } } -// Helpers +// Tools /// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) /// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// using `server_identity` as the [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) -pub fn new_tls_client_connection( +fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, + server_certificates: Option>, is_session_resumption: bool, ) -> Result { match TlsClientConnection::new(socket_connection, server_identity) { @@ -141,9 +151,19 @@ pub fn new_tls_client_connection( // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections tls_client_connection.set_require_close_notify(true); - // @TODO validate - // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation - tls_client_connection.connect_accept_certificate(|_, _, _| true); + // [TOFU](https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation) + tls_client_connection.connect_accept_certificate(move |_, c, _| { + server_certificates + .as_ref() + .is_none_or(|server_certificates| { + for server_certificate in server_certificates { + if server_certificate.is_same(c) { + return true; + } + } + false + }) + }); Ok(tls_client_connection) } diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 46576a3..f67238b 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -1,5 +1,8 @@ pub mod error; +pub mod mode; + pub use error::Error; +pub use mode::Mode; // Local dependencies @@ -10,6 +13,7 @@ use glib::{Bytes, Uri, UriHideFlags}; pub enum Request { Gemini { uri: Uri, + mode: Mode, }, Titan { uri: Uri, @@ -18,6 +22,7 @@ pub enum Request { /// but server MAY reject the request without `mime` value provided. mime: Option, token: Option, + mode: Mode, }, } @@ -27,22 +32,23 @@ impl Request { /// Generate header string for `Self` pub fn header(&self) -> String { match self { - Self::Gemini { uri } => format!("{uri}\r\n"), + Self::Gemini { uri, .. } => format!("{uri}\r\n"), Self::Titan { uri, data, mime, token, + .. } => { let mut header = format!( "{};size={}", uri.to_string_partial(UriHideFlags::QUERY), data.len() ); - if let Some(ref mime) = mime { + if let Some(mime) = mime { header.push_str(&format!(";mime={mime}")); } - if let Some(ref token) = token { + if let Some(token) = token { header.push_str(&format!(";token={token}")); } if let Some(query) = uri.query() { @@ -57,7 +63,7 @@ impl Request { /// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html) pub fn uri(&self) -> &Uri { match self { - Self::Gemini { uri } => uri, + Self::Gemini { uri, .. } => uri, Self::Titan { uri, .. } => uri, } } @@ -79,7 +85,8 @@ fn test_gemini_header() { assert_eq!( Request::Gemini { - uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() + uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(), + mode: Mode::HeaderOnly } .header(), format!("{REQUEST}\r\n") @@ -103,7 +110,8 @@ fn test_titan_header() { .unwrap(), data: Bytes::from(DATA), mime: Some(MIME.to_string()), - token: Some(TOKEN.to_string()) + token: Some(TOKEN.to_string()), + mode: Mode::HeaderOnly } .header(), format!( diff --git a/src/client/connection/request/mode.rs b/src/client/connection/request/mode.rs new file mode 100644 index 0000000..b1d8a67 --- /dev/null +++ b/src/client/connection/request/mode.rs @@ -0,0 +1,6 @@ +/// Request modes +pub enum Mode { + /// Request header bytes only, process content bytes manually + /// * useful for manual content type handle: text, stream or large content loaded by chunks + HeaderOnly, +} diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 51c3c7e..7dd54bb 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -6,7 +6,7 @@ pub mod redirect; pub mod success; pub use certificate::Certificate; -pub use error::Error; +pub use error::{Error, HeaderBytesError}; pub use failure::Failure; pub use input::Input; pub use redirect::Redirect; @@ -14,7 +14,7 @@ pub use success::Success; use super::Connection; use gio::{Cancellable, IOStream}; -use glib::{object::IsA, Priority}; +use glib::{Priority, object::IsA}; const HEADER_LEN: usize = 1024; @@ -29,13 +29,13 @@ pub enum Response { impl Response { /// Asynchronously create new `Self` for given `Connection` - pub fn from_connection_async( + pub fn header_from_connection_async( connection: Connection, priority: Priority, cancellable: Cancellable, callback: impl FnOnce(Result, Connection) + 'static, ) { - from_stream_async( + header_from_stream_async( Vec::with_capacity(HEADER_LEN), connection.stream(), cancellable, @@ -44,7 +44,7 @@ impl Response { callback( match result { Ok(buffer) => match buffer.first() { - Some(byte) => match byte { + Some(b) => match b { b'1' => match Input::from_utf8(&buffer) { Ok(input) => Ok(Self::Input(input)), Err(e) => Err(Error::Input(e)), @@ -65,16 +65,16 @@ impl Response { Ok(certificate) => Ok(Self::Certificate(certificate)), Err(e) => Err(Error::Certificate(e)), }, - _ => Err(Error::Code), + b => Err(Error::Code(*b)), }, - None => Err(Error::Protocol), + None => Err(Error::Protocol(buffer)), }, Err(e) => Err(e), }, connection, ) }, - ); + ) } } @@ -84,43 +84,63 @@ impl Response { /// /// Return UTF-8 buffer collected /// * requires `IOStream` reference to keep `Connection` active in async thread -fn from_stream_async( +fn header_from_stream_async( mut buffer: Vec, stream: impl IsA, cancellable: Cancellable, priority: Priority, - on_complete: impl FnOnce(Result, Error>) + 'static, + callback: 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)); + Ok((bytes, size)) => { + if size == 0 { + return callback(Ok(buffer)); } - - // Read next byte without record - if bytes.contains(&b'\r') { - return from_stream_async(buffer, stream, cancellable, priority, on_complete); + if buffer.len() + bytes.len() > HEADER_LEN { + buffer.extend(bytes); + return callback(Err(Error::Protocol(buffer))); } - - // Complete without record - if bytes.contains(&b'\n') { - return on_complete(Ok(buffer)); + if bytes[0] == b'\r' { + buffer.extend(bytes); + return header_from_stream_async( + buffer, + stream, + cancellable, + priority, + callback, + ); } - - // Record - buffer.append(&mut bytes); - - // Continue - from_stream_async(buffer, stream, cancellable, priority, on_complete); + if bytes[0] == b'\n' { + buffer.extend(bytes); + return callback(Ok(buffer)); + } + buffer.extend(bytes); + header_from_stream_async(buffer, stream, cancellable, priority, callback) } - Err((data, e)) => on_complete(Err(Error::Stream(e, data))), + Err((data, e)) => callback(Err(Error::Stream(e, data))), }, ) } + +/// Get header bytes slice +/// * common for all child parsers +fn header_bytes(buffer: &[u8]) -> Result<&[u8], HeaderBytesError> { + for (i, b) in buffer.iter().enumerate() { + if i > 1024 { + return Err(HeaderBytesError::Len); + } + if *b == b'\r' { + let n = i + 1; + if buffer.get(n).is_some_and(|b| *b == b'\n') { + return Ok(&buffer[..n + 1]); + } + break; + } + } + Err(HeaderBytesError::End) +} diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 160e2f0..07e2891 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) = (10, "Certificate required"); -const NOT_AUTHORIZED: (u8, &str) = (11, "Certificate not authorized"); -const NOT_VALID: (u8, &str) = (11, "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,86 @@ 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 - } - + /// Get optional message for `Self` + /// * return `None` if the message is empty (not provided by server) 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 - ) + /// 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::Required(required) => required.message_or_default(), + Self::NotAuthorized(not_authorized) => not_authorized.message_or_default(), + Self::NotValid(not_valid) => not_valid.message_or_default(), + } } -} -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), - }); + /// Get header string of `Self` + 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(), } - 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()) + /// Get header bytes of `Self` + 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(), + } } } #[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..b10d6ff --- /dev/null +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -0,0 +1,79 @@ +pub mod error; +pub use error::Error; + +/// [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code +pub const CODE: &[u8] = b"61"; + +/// 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 = "Certificate is not authorized"; + +/// Hold header `String` for [Not Authorized](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 (not provided by server) + 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 na = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap(); + assert_eq!(na.message(), Some("Not Authorized")); + assert_eq!(na.message_or_default(), "Not Authorized"); + assert_eq!(na.as_str(), "61 Not Authorized\r\n"); + assert_eq!(na.as_bytes(), "61 Not Authorized\r\n".as_bytes()); + + let na = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap(); + assert_eq!(na.message(), None); + assert_eq!(na.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(na.as_str(), "61\r\n"); + assert_eq!(na.as_bytes(), "61\r\n".as_bytes()); + + // 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..94e847c --- /dev/null +++ b/src/client/connection/response/certificate/not_valid.rs @@ -0,0 +1,79 @@ +pub mod error; +pub use error::Error; + +/// [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code +pub const CODE: &[u8] = b"62"; + +/// 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 = "Certificate is not valid"; + +/// Hold header `String` for [Not Valid](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 (not provided by server) + 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 nv = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap(); + assert_eq!(nv.message(), Some("Not Valid")); + assert_eq!(nv.message_or_default(), "Not Valid"); + assert_eq!(nv.as_str(), "62 Not Valid\r\n"); + assert_eq!(nv.as_bytes(), "62 Not Valid\r\n".as_bytes()); + + let nv = NotValid::from_utf8("62\r\n".as_bytes()).unwrap(); + assert_eq!(nv.message(), None); + assert_eq!(nv.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(nv.as_str(), "62\r\n"); + assert_eq!(nv.as_bytes(), "62\r\n".as_bytes()); + + // 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..b44585c --- /dev/null +++ b/src/client/connection/response/certificate/required.rs @@ -0,0 +1,79 @@ +pub mod error; +pub use error::Error; + +/// [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code +pub const CODE: &[u8] = b"60"; + +/// 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 = "Certificate required"; + +/// Hold header `String` for [Certificate Required](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 (not provided by server) + 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 r = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap(); + assert_eq!(r.message(), Some("Required")); + assert_eq!(r.message_or_default(), "Required"); + assert_eq!(r.as_str(), "60 Required\r\n"); + assert_eq!(r.as_bytes(), "60 Required\r\n".as_bytes()); + + let r = Required::from_utf8("60\r\n".as_bytes()).unwrap(); + assert_eq!(r.message(), None); + assert_eq!(r.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(r.as_str(), "60\r\n"); + assert_eq!(r.as_bytes(), "60\r\n".as_bytes()); + + // 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}") + } + } + } +} diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index df8cda4..022ed62 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -6,10 +6,10 @@ use std::{ #[derive(Debug)] pub enum Error { Certificate(super::certificate::Error), - Code, + Code(u8), Failure(super::failure::Error), Input(super::input::Error), - Protocol, + Protocol(Vec), Redirect(super::redirect::Error), Stream(glib::Error, Vec), Success(super::success::Error), @@ -22,8 +22,8 @@ impl Display for Error { Self::Certificate(e) => { write!(f, "Certificate error: {e}") } - Self::Code => { - write!(f, "Code group error") + Self::Code(b) => { + write!(f, "Unexpected status code byte: {b}") } Self::Failure(e) => { write!(f, "Failure error: {e}") @@ -31,7 +31,7 @@ impl Display for Error { Self::Input(e) => { write!(f, "Input error: {e}") } - Self::Protocol => { + Self::Protocol(..) => { write!(f, "Protocol error") } Self::Redirect(e) => { @@ -49,3 +49,22 @@ impl Display for Error { } } } + +#[derive(Debug)] +pub enum HeaderBytesError { + Len, + End, +} + +impl Display for HeaderBytesError { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Len => { + write!(f, "Unexpected header length") + } + Self::End => { + write!(f, "Unexpected header end") + } + } + } +} diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 40c8abf..1ace0ed 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,17 +38,52 @@ impl Failure { // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Permanent(permanent) => permanent.to_code(), - Self::Temporary(temporary) => temporary.to_code(), - } - } - + /// Get optional message for `Self` + /// * return `None` if the message is empty pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), Self::Temporary(temporary) => temporary.message(), } } + + /// 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::Permanent(permanent) => permanent.message_or_default(), + Self::Temporary(temporary) => temporary.message_or_default(), + } + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.as_str(), + Self::Temporary(temporary) => temporary.as_str(), + } + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Permanent(permanent) => permanent.as_bytes(), + Self::Temporary(temporary) => temporary.as_bytes(), + } + } +} + +#[test] +fn test() { + fn t(source: String, message: Option<&str>) { + let b = source.as_bytes(); + let i = Failure::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } + for code in [40, 41, 42, 43, 44, 50, 51, 52, 53, 59] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } 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..526a208 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,94 @@ 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; - - // 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); - - // 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); - - // 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); - - // 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); - - // 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); +fn test() { + fn t(source: String, 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); + } + for code in [50, 51, 52, 53, 59] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\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}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 768bdcd..cc20834 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -1,24 +1,31 @@ +pub mod cgi_error; +pub mod default; pub mod error; -pub use error::Error; +pub mod proxy_error; +pub mod server_unavailable; +pub mod slow_down; -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"); +pub use cgi_error::CgiError; +pub use default::Default; +pub use error::Error; +pub use proxy_error::ProxyError; +pub use server_unavailable::ServerUnavailable; +pub use slow_down::SlowDown; + +const CODE: u8 = b'4'; /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure pub enum Temporary { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40 - Default { message: Option }, + Default(Default), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable { message: Option }, + ServerUnavailable(ServerUnavailable), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError { message: Option }, + CgiError(CgiError), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError { message: Option }, + ProxyError(ProxyError), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown { message: Option }, + SlowDown(SlowDown), } impl Temporary { @@ -26,154 +33,94 @@ impl Temporary { /// 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::ServerUnavailable( + ServerUnavailable::from_utf8(buffer) + .map_err(Error::ServerUnavailable)?, + )), + b'2' => Ok(Self::CgiError( + CgiError::from_utf8(buffer).map_err(Error::CgiError)?, + )), + b'3' => Ok(Self::ProxyError( + ProxyError::from_utf8(buffer).map_err(Error::ProxyError)?, + )), + b'4' => Ok(Self::SlowDown( + SlowDown::from_utf8(buffer).map_err(Error::SlowDown)?, + )), + 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::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, + Self::Default(default) => default.message(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.message(), + Self::CgiError(cgi_error) => cgi_error.message(), + Self::ProxyError(proxy_error) => proxy_error.message(), + Self::SlowDown(slow_down) => slow_down.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 { .. } => DEFAULT, - Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, - Self::CgiError { .. } => CGI_ERROR, - Self::ProxyError { .. } => PROXY_ERROR, - Self::SlowDown { .. } => SLOW_DOWN, - } - .1 - ) + /// 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::ServerUnavailable(server_unavailable) => server_unavailable.message_or_default(), + Self::CgiError(cgi_error) => cgi_error.message_or_default(), + Self::ProxyError(proxy_error) => proxy_error.message_or_default(), + Self::SlowDown(slow_down) => slow_down.message_or_default(), + } } -} -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), - }); + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.as_str(), + Self::CgiError(cgi_error) => cgi_error.as_str(), + Self::ProxyError(proxy_error) => proxy_error.as_str(), + Self::SlowDown(slow_down) => slow_down.as_str(), } - 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()) + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.as_bytes(), + Self::CgiError(cgi_error) => cgi_error.as_bytes(), + Self::ProxyError(proxy_error) => proxy_error.as_bytes(), + Self::SlowDown(slow_down) => slow_down.as_bytes(), + } } } #[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); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Temporary::from_str("40\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - // 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); - assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); - - 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); - assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); - - // 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); - assert_eq!(cgi_error.to_string(), CGI_ERROR.1); - - 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); - assert_eq!(cgi_error.to_string(), CGI_ERROR.1); - - // 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); - assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); - - 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); - assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); - - // 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); - assert_eq!(slow_down.to_string(), SLOW_DOWN.1); - - 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); - assert_eq!(slow_down.to_string(), SLOW_DOWN.1); +fn test() { + fn t(source: String, message: Option<&str>) { + let b = source.as_bytes(); + let i = Temporary::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } + for code in [40, 41, 42, 43, 44] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } diff --git a/src/client/connection/response/failure/temporary/cgi_error.rs b/src/client/connection/response/failure/temporary/cgi_error.rs new file mode 100644 index 0000000..8843fa9 --- /dev/null +++ b/src/client/connection/response/failure/temporary/cgi_error.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code +pub const CODE: &[u8] = b"42"; + +/// 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 = "CGI Error"; + +/// Hold header `String` for [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct CgiError(String); + +impl CgiError { + // 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 ce = CgiError::from_utf8("42 Message\r\n".as_bytes()).unwrap(); + assert_eq!(ce.message(), Some("Message")); + assert_eq!(ce.message_or_default(), "Message"); + assert_eq!(ce.as_str(), "42 Message\r\n"); + assert_eq!(ce.as_bytes(), "42 Message\r\n".as_bytes()); + + let ce = CgiError::from_utf8("42\r\n".as_bytes()).unwrap(); + assert_eq!(ce.message(), None); + assert_eq!(ce.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(ce.as_str(), "42\r\n"); + assert_eq!(ce.as_bytes(), "42\r\n".as_bytes()); + + // err + assert!(CgiError::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(CgiError::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(CgiError::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/cgi_error/error.rs b/src/client/connection/response/failure/temporary/cgi_error/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/cgi_error/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/temporary/default.rs b/src/client/connection/response/failure/temporary/default.rs new file mode 100644 index 0000000..e56d90b --- /dev/null +++ b/src/client/connection/response/failure/temporary/default.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code +pub const CODE: &[u8] = b"40"; + +/// 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 = "Temporary error"; + +/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) 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("40 Message\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Message")); + assert_eq!(d.message_or_default(), "Message"); + assert_eq!(d.as_str(), "40 Message\r\n"); + assert_eq!(d.as_bytes(), "40 Message\r\n".as_bytes()); + + let d = Default::from_utf8("40\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "40\r\n"); + assert_eq!(d.as_bytes(), "40\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/temporary/default/error.rs b/src/client/connection/response/failure/temporary/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/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/temporary/error.rs b/src/client/connection/response/failure/temporary/error.rs index 5cf1cf6..afa9154 100644 --- a/src/client/connection/response/failure/temporary/error.rs +++ b/src/client/connection/response/failure/temporary/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), + CgiError(super::cgi_error::Error), + Default(super::default::Error), + FirstByte(u8), + ProxyError(super::proxy_error::Error), + SecondByte(u8), + ServerUnavailable(super::server_unavailable::Error), + SlowDown(super::slow_down::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::CgiError(e) => { + write!(f, "CgiError 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::ProxyError(e) => { + write!(f, "ProxyError parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::ServerUnavailable(e) => { + write!(f, "ServerUnavailable parse error: {e}") + } + Self::SlowDown(e) => { + write!(f, "SlowDown parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/failure/temporary/proxy_error.rs b/src/client/connection/response/failure/temporary/proxy_error.rs new file mode 100644 index 0000000..1264c34 --- /dev/null +++ b/src/client/connection/response/failure/temporary/proxy_error.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code +pub const CODE: &[u8] = b"43"; + +/// 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 error"; + +/// Hold header `String` for [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ProxyError(String); + +impl ProxyError { + // 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 pe = ProxyError::from_utf8("43 Message\r\n".as_bytes()).unwrap(); + assert_eq!(pe.message(), Some("Message")); + assert_eq!(pe.message_or_default(), "Message"); + assert_eq!(pe.as_str(), "43 Message\r\n"); + assert_eq!(pe.as_bytes(), "43 Message\r\n".as_bytes()); + + let pe = ProxyError::from_utf8("43\r\n".as_bytes()).unwrap(); + assert_eq!(pe.message(), None); + assert_eq!(pe.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(pe.as_str(), "43\r\n"); + assert_eq!(pe.as_bytes(), "43\r\n".as_bytes()); + + // err + assert!(ProxyError::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ProxyError::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ProxyError::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/proxy_error/error.rs b/src/client/connection/response/failure/temporary/proxy_error/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/proxy_error/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/temporary/server_unavailable.rs b/src/client/connection/response/failure/temporary/server_unavailable.rs new file mode 100644 index 0000000..f42802e --- /dev/null +++ b/src/client/connection/response/failure/temporary/server_unavailable.rs @@ -0,0 +1,81 @@ +pub mod error; +pub use error::Error; + +/// [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable) +/// temporary error status code +pub const CODE: &[u8] = b"41"; + +/// 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 = "Server unavailable"; + +/// Hold header `String` for [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable) +/// temporary error status code +/// +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ServerUnavailable(String); + +impl ServerUnavailable { + // 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 su = ServerUnavailable::from_utf8("41 Message\r\n".as_bytes()).unwrap(); + assert_eq!(su.message(), Some("Message")); + assert_eq!(su.message_or_default(), "Message"); + assert_eq!(su.as_str(), "41 Message\r\n"); + assert_eq!(su.as_bytes(), "41 Message\r\n".as_bytes()); + + let su = ServerUnavailable::from_utf8("41\r\n".as_bytes()).unwrap(); + assert_eq!(su.message(), None); + assert_eq!(su.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(su.as_str(), "41\r\n"); + assert_eq!(su.as_bytes(), "41\r\n".as_bytes()); + + // err + assert!(ServerUnavailable::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ServerUnavailable::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ServerUnavailable::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/server_unavailable/error.rs b/src/client/connection/response/failure/temporary/server_unavailable/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/server_unavailable/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/temporary/slow_down.rs b/src/client/connection/response/failure/temporary/slow_down.rs new file mode 100644 index 0000000..3ca346d --- /dev/null +++ b/src/client/connection/response/failure/temporary/slow_down.rs @@ -0,0 +1,81 @@ +pub mod error; +pub use error::Error; + +/// [Slow Down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down) +/// temporary error status code +pub const CODE: &[u8] = b"44"; + +/// 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 = "Slow down"; + +/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down) +/// temporary error status code +/// +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct SlowDown(String); + +impl SlowDown { + // 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 sd = SlowDown::from_utf8("44 Message\r\n".as_bytes()).unwrap(); + assert_eq!(sd.message(), Some("Message")); + assert_eq!(sd.message_or_default(), "Message"); + assert_eq!(sd.as_str(), "44 Message\r\n"); + assert_eq!(sd.as_bytes(), "44 Message\r\n".as_bytes()); + + let sd = SlowDown::from_utf8("44\r\n".as_bytes()).unwrap(); + assert_eq!(sd.message(), None); + assert_eq!(sd.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(sd.as_str(), "44\r\n"); + assert_eq!(sd.as_bytes(), "44\r\n".as_bytes()); + + // err + assert!(SlowDown::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(SlowDown::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(SlowDown::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/slow_down/error.rs b/src/client/connection/response/failure/temporary/slow_down/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/slow_down/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index b62276b..0cd9857 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -1,12 +1,17 @@ +pub mod default; pub mod error; +pub mod sensitive; + +pub use default::Default; pub use error::Error; +pub use sensitive::Sensitive; -const DEFAULT: (u8, &str) = (10, "Input"); -const SENSITIVE: (u8, &str) = (11, "Sensitive input"); +const CODE: u8 = b'1'; +/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected) pub enum Input { - Default { message: Option }, - Sensitive { message: Option }, + Default(Default), + Sensitive(Sensitive), } impl Input { @@ -14,97 +19,74 @@ impl Input { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::Sensitive( + Sensitive::from_utf8(buffer).map_err(Error::Sensitive)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Default { .. } => DEFAULT, - Self::Sensitive { .. } => SENSITIVE, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::Sensitive { message } => message, + Self::Default(default) => default.message(), + Self::Sensitive(sensitive) => sensitive.message(), } - .as_deref() } -} -impl std::fmt::Display for Input { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Default { .. } => DEFAULT, - Self::Sensitive { .. } => SENSITIVE, - } - .1 - ) - } -} - -impl std::str::FromStr for Input { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("10") { - return Ok(Self::Default { - message: message(postfix), - }); + /// 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::Sensitive(sensitive) => sensitive.message_or_default(), } - if let Some(postfix) = header.strip_prefix("11") { - return Ok(Self::Sensitive { - message: message(postfix), - }); - } - Err(Error::Protocol) } -} -// Tools + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::Sensitive(sensitive) => sensitive.as_str(), + } + } -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::Sensitive(sensitive) => sensitive.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 = Input::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } // 10 - let default = Input::from_str("10 Default\r\n").unwrap(); - assert_eq!(default.message(), Some("Default")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Input::from_str("10\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - + t("10 Default\r\n", Some("Default")); + t("10\r\n", None); // 11 - let sensitive = Input::from_str("11 Sensitive\r\n").unwrap(); - assert_eq!(sensitive.message(), Some("Sensitive")); - assert_eq!(sensitive.to_code(), SENSITIVE.0); - assert_eq!(sensitive.to_string(), SENSITIVE.1); - - let sensitive = Input::from_str("11\r\n").unwrap(); - assert_eq!(sensitive.message(), None); - assert_eq!(sensitive.to_code(), SENSITIVE.0); - assert_eq!(sensitive.to_string(), SENSITIVE.1); + t("11 Sensitive\r\n", Some("Sensitive")); + t("11\r\n", None); } diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs new file mode 100644 index 0000000..4a5a3df --- /dev/null +++ b/src/client/connection/response/input/default.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +pub const CODE: &[u8] = b"10"; + +/// 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 = "Input expected"; + +/// Hold header `String` for [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// 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("10 Default\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Default")); + assert_eq!(d.message_or_default(), "Default"); + assert_eq!(d.as_str(), "10 Default\r\n"); + assert_eq!(d.as_bytes(), "10 Default\r\n".as_bytes()); + + let d = Default::from_utf8("10\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "10\r\n"); + assert_eq!(d.as_bytes(), "10\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/input/default/error.rs b/src/client/connection/response/input/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/input/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index ae589e8..6763727 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -1,22 +1,35 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Protocol, - Utf8Error(Utf8Error), + Default(super::default::Error), + FirstByte(u8), + SecondByte(u8), + Sensitive(super::sensitive::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") } - Self::Protocol => { - write!(f, "Protocol error") + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::Sensitive(e) => { + write!(f, "Sensitive parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs new file mode 100644 index 0000000..594c8fb --- /dev/null +++ b/src/client/connection/response/input/sensitive.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +pub const CODE: &[u8] = b"11"; + +/// 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 = "Sensitive input expected"; + +/// Hold header `String` for [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Sensitive(String); + +impl Sensitive { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// 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 s = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); + assert_eq!(s.message(), Some("Sensitive")); + assert_eq!(s.message_or_default(), "Sensitive"); + assert_eq!(s.as_str(), "11 Sensitive\r\n"); + assert_eq!(s.as_bytes(), "11 Sensitive\r\n".as_bytes()); + + let s = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); + assert_eq!(s.message(), None); + assert_eq!(s.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(s.as_str(), "11\r\n"); + assert_eq!(s.as_bytes(), "11\r\n".as_bytes()); + + // err + assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/input/sensitive/error.rs b/src/client/connection/response/input/sensitive/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/input/sensitive/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index add308c..48bd610 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -1,16 +1,23 @@ pub mod error; -pub use error::Error; +pub mod permanent; +pub mod temporary; -use glib::{GStringPtr, Uri, UriFlags}; +pub use error::{Error, UriError}; +pub use permanent::Permanent; +pub use temporary::Temporary; -const TEMPORARY: (u8, &str) = (30, "Temporary redirect"); -const PERMANENT: (u8, &str) = (31, "Permanent redirect"); +// Local dependencies +use glib::{Uri, UriFlags}; + +const CODE: u8 = b'3'; + +/// [Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) statuses pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - Temporary { target: String }, + Temporary(Temporary), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Permanent { target: String }, + Permanent(Permanent), } impl Redirect { @@ -18,134 +25,121 @@ impl Redirect { /// 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, - } - .0 - } - - /// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection), - /// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base` - /// * fragment implementation uncompleted @TODO - pub fn to_uri(&self, base: &Uri) -> Result { - match Uri::build( - UriFlags::NONE, - base.scheme().as_str(), - None, // unexpected - base.host().as_deref(), - base.port(), - base.path().as_str(), - // > If a server sends a redirection in response to a request with a query string, - // > the client MUST NOT apply the query string to the new location - None, - // > A server SHOULD NOT include fragments in redirections, - // > but if one is given, and a client already has a fragment it could apply (from the original URI), - // > it is up to the client which fragment to apply. - None, // @TODO - ) - .parse_relative(self.target(), UriFlags::NONE) - { - Ok(absolute) => Ok(absolute), - Err(e) => Err(Error::Uri(e)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Temporary( + Temporary::from_utf8(buffer).map_err(Error::Temporary)?, + )), + b'1' => Ok(Self::Permanent( + Permanent::from_utf8(buffer).map_err(Error::Permanent)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn target(&self) -> &str { + pub fn target(&self) -> Result<&str, Error> { match self { - Self::Permanent { target } => target, - Self::Temporary { target } => target, + Self::Temporary(temporary) => temporary.target().map_err(Error::Temporary), + Self::Permanent(permanent) => permanent.target().map_err(Error::Permanent), + } + } + + pub fn as_str(&self) -> &str { + match self { + Self::Temporary(temporary) => temporary.as_str(), + Self::Permanent(permanent) => permanent.as_str(), + } + } + + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Temporary(temporary) => temporary.as_bytes(), + Self::Permanent(permanent) => permanent.as_bytes(), + } + } + + pub fn uri(&self, base: &Uri) -> Result { + match self { + Self::Temporary(temporary) => temporary.uri(base).map_err(Error::Temporary), + Self::Permanent(permanent) => permanent.uri(base).map_err(Error::Permanent), } } } -impl std::fmt::Display for Redirect { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Permanent { .. } => PERMANENT, - Self::Temporary { .. } => TEMPORARY, +// Tools + +/// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection), +/// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base` +/// * fragment implementation uncompleted @TODO +fn uri(target: &str, base: &Uri) -> Result { + match Uri::build( + UriFlags::NONE, + base.scheme().as_str(), + None, // unexpected + base.host().as_deref(), + base.port(), + base.path().as_str(), + // > If a server sends a redirection in response to a request with a query string, + // > the client MUST NOT apply the query string to the new location + None, + // > A server SHOULD NOT include fragments in redirections, + // > but if one is given, and a client already has a fragment it could apply (from the original URI), + // > it is up to the client which fragment to apply. + None, // @TODO + ) + .parse_relative( + &{ + // URI started with double slash yet not supported by Glib function + // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + let t = target; + match t.strip_prefix("//") { + Some(p) => { + let postfix = p.trim_start_matches(":"); + format!( + "{}://{}", + base.scheme(), + if postfix.is_empty() { + match base.host() { + Some(h) => format!("{h}/"), + None => return Err(UriError::BaseHost), + } + } else { + postfix.to_string() + } + ) + } + None => t.to_string(), } - .1 - ) - } -} - -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), + }, + UriFlags::NONE, + ) { + Ok(absolute) => Ok(absolute), + Err(e) => Err(UriError::ParseRelative(e)), } } #[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.0); - assert_eq!(temporary.to_string(), TEMPORARY.1); - - let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); - assert_eq!(permanent.target(), "/uri"); - assert_eq!(permanent.to_code(), PERMANENT.0); - assert_eq!(permanent.to_string(), PERMANENT.1); -} - -#[test] -fn test_to_uri() { - use std::str::FromStr; - - let request = Uri::build( +fn test() { + /// Test common assertion rules + fn t(base: &Uri, source: &str, target: &str) { + let b = source.as_bytes(); + let r = Redirect::from_utf8(b).unwrap(); + assert!(r.uri(base).is_ok_and(|u| u.to_string() == target)); + assert_eq!(r.as_str(), source); + assert_eq!(r.as_bytes(), b); + } + // common base + let base = Uri::build( UriFlags::NONE, "gemini", None, @@ -155,22 +149,37 @@ fn test_to_uri() { Some("query"), Some("fragment"), ); - - let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://geminiprotocol.net/uri" + // codes test + t( + &base, + "30 gemini://geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", ); - - let resolve = Redirect::from_str("30 uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://geminiprotocol.net/path/uri" + t( + &base, + "31 gemini://geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", ); - - let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://test.host/uri" + // relative test + t( + &base, + "31 path\r\n", + "gemini://geminiprotocol.net/path/path", ); + t( + &base, + "31 //geminiprotocol.net\r\n", + "gemini://geminiprotocol.net", + ); + t( + &base, + "31 //geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", + ); + t(&base, "31 /path\r\n", "gemini://geminiprotocol.net/path"); + t(&base, "31 //:\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 //\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 /\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 ../\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 ..\r\n", "gemini://geminiprotocol.net/"); } diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index acee073..38aaab1 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -1,30 +1,55 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Uri(glib::Error), - Protocol, - Target, - Utf8Error(Utf8Error), + FirstByte(u8), + Permanent(super::permanent::Error), + SecondByte(u8), + Temporary(super::temporary::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Uri(e) => { - write!(f, "URI error: {e}") + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Permanent(e) => { + write!(f, "Permanent parse error: {e}") } - Self::Protocol => { - write!(f, "Protocol error") + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") } - Self::Target => { - write!(f, "Target error") + Self::Temporary(e) => { + write!(f, "Temporary parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") + } + } + } +} + +/// Handle `super::uri` method +#[derive(Debug)] +pub enum UriError { + BaseHost, + ParseRelative(glib::Error), +} + +impl Display for UriError { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::BaseHost => { + write!(f, "URI base host required") + } + Self::ParseRelative(e) => { + write!(f, "URI parse relative error: {e}") } } } diff --git a/src/client/connection/response/redirect/permanent.rs b/src/client/connection/response/redirect/permanent.rs new file mode 100644 index 0000000..e8e6371 --- /dev/null +++ b/src/client/connection/response/redirect/permanent.rs @@ -0,0 +1,82 @@ +pub mod error; +pub use error::Error; + +// Local dependencies + +use glib::Uri; + +/// [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code +pub const CODE: &[u8] = b"31"; + +/// Hold header `String` for [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Permanent(String); + +impl Permanent { + // 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 raw target for `Self` + /// * return `Err` if the required target is empty + pub fn target(&self) -> Result<&str, Error> { + self.0 + .get(2..) + .map(|s| s.trim()) + .filter(|x| !x.is_empty()) + .ok_or(Error::TargetEmpty) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub fn uri(&self, base: &Uri) -> Result { + super::uri(self.target()?, base).map_err(Error::Uri) + } +} + +#[test] +fn test() { + const BUFFER: &str = "31 gemini://geminiprotocol.net/path\r\n"; + let bytes = BUFFER.as_bytes(); + let base = Uri::build( + glib::UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); + let permanent = Permanent::from_utf8(bytes).unwrap(); + assert_eq!(permanent.as_str(), BUFFER); + assert_eq!(permanent.as_bytes(), bytes); + assert!(permanent.target().is_ok()); + assert!( + permanent + .uri(&base) + .is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path") + ); + assert!(Permanent::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/redirect/permanent/error.rs b/src/client/connection/response/redirect/permanent/error.rs new file mode 100644 index 0000000..0d0e81d --- /dev/null +++ b/src/client/connection/response/redirect/permanent/error.rs @@ -0,0 +1,32 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + TargetEmpty, + Uri(super::super::UriError), + 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::TargetEmpty => { + write!(f, "Expected target is empty") + } + Self::Uri(e) => { + write!(f, "URI parse error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/redirect/temporary.rs b/src/client/connection/response/redirect/temporary.rs new file mode 100644 index 0000000..a131336 --- /dev/null +++ b/src/client/connection/response/redirect/temporary.rs @@ -0,0 +1,82 @@ +pub mod error; +pub use error::Error; + +// Local dependencies + +use glib::Uri; + +/// [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code +pub const CODE: &[u8] = b"30"; + +/// Hold header `String` for [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Temporary(String); + +impl Temporary { + // 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 raw target for `Self` + /// * return `Err` if the required target is empty + pub fn target(&self) -> Result<&str, Error> { + self.0 + .get(2..) + .map(|s| s.trim()) + .filter(|x| !x.is_empty()) + .ok_or(Error::TargetEmpty) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub fn uri(&self, base: &Uri) -> Result { + super::uri(self.target()?, base).map_err(Error::Uri) + } +} + +#[test] +fn test() { + const BUFFER: &str = "30 gemini://geminiprotocol.net/path\r\n"; + let bytes = BUFFER.as_bytes(); + let base = Uri::build( + glib::UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); + let temporary = Temporary::from_utf8(BUFFER.as_bytes()).unwrap(); + assert_eq!(temporary.as_str(), BUFFER); + assert_eq!(temporary.as_bytes(), bytes); + assert!(temporary.target().is_ok()); + assert!( + temporary + .uri(&base) + .is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path") + ); + assert!(Temporary::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err()) +} diff --git a/src/client/connection/response/redirect/temporary/error.rs b/src/client/connection/response/redirect/temporary/error.rs new file mode 100644 index 0000000..0d0e81d --- /dev/null +++ b/src/client/connection/response/redirect/temporary/error.rs @@ -0,0 +1,32 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + TargetEmpty, + Uri(super::super::UriError), + 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::TargetEmpty => { + write!(f, "Expected target is empty") + } + Self::Uri(e) => { + write!(f, "URI parse error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index e5ad6f4..f9493d6 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -1,89 +1,76 @@ +pub mod default; pub mod error; + +pub use default::Default; pub use error::Error; -const DEFAULT: (u8, &str) = (20, "Success"); +const CODE: u8 = b'2'; pub enum Success { - Default { mime: String }, + Default(Default), // reserved for 2* codes } impl Success { // Constructors - /// Create new `Self` from buffer include header bytes + /// Parse new `Self` from buffer 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)), + if buffer.first().is_none_or(|b| *b != CODE) { + return Err(Error::Code); } - } - - // Convertors - - pub fn to_code(&self) -> u8 { - match self { - Self::Default { .. } => DEFAULT.0, + match Default::from_utf8(buffer) { + Ok(default) => Ok(Self::Default(default)), + Err(e) => Err(Error::Default(e)), } } // Getters - pub fn mime(&self) -> &str { + /// Get header bytes for `Self` type + pub fn as_header_bytes(&self) -> &[u8] { match self { - Self::Default { mime } => mime, + Self::Default(default) => default.header.as_bytes(), } } -} -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, - } - ) + /// Get header string for `Self` type + pub fn as_header_str(&self) -> &str { + match self { + Self::Default(default) => default.header.as_str(), + } } -} -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), + /// Get parsed MIME for `Self` type + /// + /// * high-level method, useful to skip extra match case constructions; + /// * at this moment, Gemini protocol has only one status code in this scope,\ + /// this method would be deprecated in future, use on your own risk! + pub fn mime(&self) -> Result { + match self { + Self::Default(default) => default + .header + .mime() + .map_err(|e| Error::Default(default::Error::Header(e))), } } } #[test] -fn test_from_str() { - use std::str::FromStr; +fn test() { + let r = "20 text/gemini; charset=utf-8; lang=en\r\n"; + let b = r.as_bytes(); + let s = Success::from_utf8(b).unwrap(); - let default = Success::from_str("20 text/gemini; charset=utf-8; lang=en\r\n").unwrap(); + match s { + Success::Default(ref d) => { + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert!(d.content.is_empty()) + } + } + assert_eq!(s.as_header_bytes(), b); + assert_eq!(s.as_header_str(), r); + assert_eq!(s.mime().unwrap(), "text/gemini"); - assert_eq!(default.mime(), "text/gemini"); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); + assert!(Success::from_utf8("40 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err()) } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs new file mode 100644 index 0000000..488c3e6 --- /dev/null +++ b/src/client/connection/response/success/default.rs @@ -0,0 +1,51 @@ +pub mod error; +pub mod header; + +pub use error::Error; +pub use header::Header; + +/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code +pub const CODE: &[u8] = b"20"; + +/// Holder for [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code +/// * this response type MAY contain body data +/// * the header has closed members to require valid construction +pub struct Default { + /// Formatted header holder with additional API + pub header: Header, + /// Default success response MAY include body data + /// * if the `Request` constructed with `Mode::HeaderOnly` flag,\ + /// this value wants to be processed manually, using external application logic (specific for content-type) + pub content: Vec, +} + +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); + } + let header = Header::from_utf8(buffer).map_err(Error::Header)?; + Ok(Self { + content: buffer + .get(header.as_bytes().len()..) + .filter(|s| !s.is_empty()) + .map_or(Vec::new(), |v| v.to_vec()), + header, + }) + } +} + +#[test] +fn test() { + let d = Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert!(d.content.is_empty()); + + let d = + Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\ndata".as_bytes()).unwrap(); + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert_eq!(d.content.len(), 4); +} diff --git a/src/client/connection/response/success/default/error.rs b/src/client/connection/response/success/default/error.rs new file mode 100644 index 0000000..d5b28b5 --- /dev/null +++ b/src/client/connection/response/success/default/error.rs @@ -0,0 +1,20 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(super::header::Error), +} + +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}") + } + } + } +} diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs new file mode 100644 index 0000000..dab58b7 --- /dev/null +++ b/src/client/connection/response/success/default/header.rs @@ -0,0 +1,60 @@ +pub mod error; +pub use error::Error; + +pub struct Header(String); + +impl Header { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(super::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 + + /// Parse content type for `Self` + pub fn mime(&self) -> Result { + glib::Regex::split_simple( + r"^\d{2}\s([^\/]+\/[^\s;]+)", + &self.0, + glib::RegexCompileFlags::DEFAULT, + glib::RegexMatchFlags::DEFAULT, + ) + .get(1) + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map_or(Err(Error::Mime), |s| Ok(s.to_lowercase())) + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +#[test] +fn test() { + let s = "20 text/gemini; charset=utf-8; lang=en\r\n"; + let b = s.as_bytes(); + let h = Header::from_utf8(b).unwrap(); + assert_eq!(h.mime().unwrap(), "text/gemini"); + assert_eq!(h.as_bytes(), b); + assert_eq!(h.as_str(), s); + + assert!(Header::from_utf8("21 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/success/default/header/error.rs b/src/client/connection/response/success/default/header/error.rs new file mode 100644 index 0000000..4daca3a --- /dev/null +++ b/src/client/connection/response/success/default/header/error.rs @@ -0,0 +1,31 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Code, + Mime, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Mime => { + write!(f, "Unexpected content type") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + } + } +} diff --git a/src/client/connection/response/success/error.rs b/src/client/connection/response/success/error.rs index 2dbe363..fe32c5f 100644 --- a/src/client/connection/response/success/error.rs +++ b/src/client/connection/response/success/error.rs @@ -1,26 +1,19 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Protocol, - Mime, - Utf8Error(Utf8Error), + Code, + Default(super::default::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Code => { + write!(f, "Unexpected status code") } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Mime => { - write!(f, "MIME error") + Self::Default(e) => { + write!(f, "Header error: {e}") } } } diff --git a/src/client/error.rs b/src/client/error.rs index 6083e77..73031da 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -2,22 +2,29 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Connect(glib::Error), - Connection(crate::client::connection::Error), - Request(crate::client::connection::request::Error), + Connect(gio::NetworkAddress, glib::Error), + Connection(gio::SocketConnection, crate::client::connection::Error), + NetworkAddress(crate::client::connection::request::Error), + Request( + crate::client::connection::Connection, + crate::client::connection::Error, + ), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Connection(e) => { - write!(f, "Connection error: {e}") - } - Self::Connect(e) => { + Self::Connect(_, e) => { write!(f, "Connect error: {e}") } - Self::Request(e) => { - write!(f, "Request error: {e}") + Self::Connection(_, e) => { + write!(f, "Connection init error: {e}") + } + Self::NetworkAddress(e) => { + write!(f, "Network address error: {e}") + } + Self::Request(_, e) => { + write!(f, "Connection error: {e}") } } } diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 9d6ef23..2dffb5e 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -1,11 +1,14 @@ pub mod error; +pub mod size; + pub use error::Error; +pub use size::Size; use gio::{ - prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual}, Cancellable, FileOutputStream, IOStream, + prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual}, }; -use glib::{object::IsA, Bytes, Priority}; +use glib::{Bytes, Priority, object::IsA}; /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) @@ -15,59 +18,50 @@ pub fn from_stream_async( file_output_stream: FileOutputStream, cancellable: Cancellable, priority: Priority, - (chunk, limit, mut total): ( - usize, // bytes_in_chunk - Option, // bytes_total_limit, `None` to unlimited - usize, // bytes_total - ), + mut size: Size, (on_chunk, on_complete): ( impl Fn(Bytes, usize) + 'static, // on_chunk impl FnOnce(Result<(FileOutputStream, usize), Error>) + 'static, // on_complete ), ) { io_stream.input_stream().read_bytes_async( - chunk, + size.chunk, priority, Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - total += bytes.len(); - on_chunk(bytes.clone(), total); + size.total += bytes.len(); + on_chunk(bytes.clone(), size.total); - if let Some(limit) = limit { - if total > limit { - return on_complete(Err(Error::BytesTotal(total, limit))); - } + if let Some(limit) = size.limit + && size.total > limit + { + return on_complete(Err(Error::BytesTotal(size.total, limit))); } - if bytes.len() == 0 { - return on_complete(Ok((file_output_stream, total))); + if bytes.is_empty() { + return on_complete(Ok((file_output_stream, size.total))); } // Make sure **all bytes** sent to the destination // > A partial write is performed with the size of a message block, which is 16kB // > https://docs.openssl.org/3.0/man3/SSL_write/#notes file_output_stream.clone().write_all_async( - bytes.clone(), + bytes, priority, Some(&cancellable.clone()), - move |result| { - match result { - Ok(_) => { - // continue read.. - from_stream_async( - io_stream, - file_output_stream, - cancellable, - priority, - (chunk, limit, total), - (on_chunk, on_complete), - ); - } - Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))), - } + move |result| match result { + Ok(_) => from_stream_async( + io_stream, + file_output_stream, + cancellable, + priority, + size, + (on_chunk, on_complete), + ), + Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))), }, - ); + ) } Err(e) => on_complete(Err(Error::InputStream(e))), }, diff --git a/src/gio/file_output_stream/size.rs b/src/gio/file_output_stream/size.rs new file mode 100644 index 0000000..5d0c911 --- /dev/null +++ b/src/gio/file_output_stream/size.rs @@ -0,0 +1,17 @@ +/// Mutable bytes count +pub struct Size { + pub chunk: usize, + /// `None` for unlimited + pub limit: Option, + pub total: usize, +} + +impl Default for Size { + fn default() -> Self { + Self { + chunk: 0x10000, // 64KB + limit: None, + total: 0, + } + } +} diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index def3845..2b1fc39 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -1,11 +1,14 @@ pub mod error; +pub mod size; + pub use error::Error; +pub use size::Size; use gio::{ - prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, Cancellable, IOStream, MemoryInputStream, + prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, }; -use glib::{object::IsA, Priority}; +use glib::{Priority, object::IsA}; /// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) @@ -17,7 +20,7 @@ pub fn from_stream_async( io_stream: impl IsA, priority: Priority, cancelable: Cancellable, - (chunk, limit): (usize, usize), + size: Size, (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, @@ -28,7 +31,7 @@ pub fn from_stream_async( io_stream, priority, cancelable, - (chunk, limit, 0), + size, (on_chunk, on_complete), ); } @@ -41,14 +44,14 @@ pub fn for_memory_input_stream_async( io_stream: impl IsA, priority: Priority, cancellable: Cancellable, - (chunk, limit, mut total): (usize, usize, usize), + mut size: Size, (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { io_stream.input_stream().read_bytes_async( - chunk, + size.chunk, priority, Some(&cancellable.clone()), move |result| match result { @@ -57,19 +60,23 @@ pub fn for_memory_input_stream_async( // is end of stream if len == 0 { - return on_complete(Ok((memory_input_stream, total))); + return on_complete(Ok((memory_input_stream, size.total))); } // callback chunk function - total += len; - on_chunk(len, total); + size.total += len; + on_chunk(len, size.total); // push bytes into the memory pool memory_input_stream.add_bytes(&bytes); // prevent memory overflow - if total > limit { - return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); + if size.total > size.limit { + return on_complete(Err(Error::BytesTotal( + memory_input_stream, + size.total, + size.limit, + ))); } // handle next chunk.. @@ -78,7 +85,7 @@ pub fn for_memory_input_stream_async( io_stream, priority, cancellable, - (chunk, limit, total), + size, (on_chunk, on_complete), ) } diff --git a/src/gio/memory_input_stream/size.rs b/src/gio/memory_input_stream/size.rs new file mode 100644 index 0000000..9a10bd3 --- /dev/null +++ b/src/gio/memory_input_stream/size.rs @@ -0,0 +1,16 @@ +/// Mutable bytes count +pub struct Size { + pub chunk: usize, + pub limit: usize, + pub total: usize, +} + +impl Default for Size { + fn default() -> Self { + Self { + chunk: 0x10000, // 64KB + limit: 0xfffff, // 1 MB + total: 0, + } + } +}