diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index ada8a24..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -custom: https://yggverse.github.io/#donate \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/rust.yml similarity index 50% rename from .github/workflows/build.yml rename to .github/workflows/rust.yml index ad7ed4d..9fd45e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/rust.yml @@ -1,4 +1,4 @@ -name: Build +name: Rust on: push: @@ -8,7 +8,6 @@ on: env: CARGO_TERM_COLOR: always - RUSTFLAGS: -Dwarnings jobs: build: @@ -17,14 +16,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Run rustfmt - run: cargo fmt --all -- --check - - name: Update packages index - run: sudo apt update - - name: Install system packages - run: sudo apt install -y libglib2.0-dev - - name: Run clippy - run: cargo clippy --all-targets - name: Build run: cargo build --verbose - name: Run tests diff --git a/Cargo.toml b/Cargo.toml index 801fb2e..538c997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,20 @@ [package] name = "ggemini" -version = "0.20.1" -edition = "2024" +version = "0.11.0" +edition = "2021" license = "MIT" readme = "README.md" description = "Glib/Gio-oriented network API for Gemini protocol" -keywords = ["gemini", "titan", "glib", "gio", "client"] +keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"] categories = ["development-tools", "network-programming", "parsing"] repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.21.0" +version = "0.20.4" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.21.0" +version = "0.20.4" features = ["v2_66"] diff --git a/README.md b/README.md index 03c633e..60746a0 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,12 @@ # ggemini - -[](https://docs.rs/ggemini) -[](https://crates.io/crates/ggemini) - Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) / [gio](https://crates.io/crates/gio) (`2.66+`) backend. - -## Requirements - - -Debian - -sudo apt install libglib2.0-dev - - - -Fedora - -sudo dnf install glib2-devel - +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependency. ## Install @@ -39,39 +21,42 @@ cargo add ggemini ### Example ``` rust -use gio::*; -use glib::*; +use gtk::gio::*; +use gtk::glib::*; use ggemini::client::{ - connection::{request::{Mode, Request}, Response}, - Client, + connection::{ + response::meta::{Mime, Status}, + Response, + }, + Client, Error, }; 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) - }, + Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), Priority::DEFAULT, Cancellable::new(), - None, // optional auth `GTlsCertificate` - None, // optional TOFU `GTlsCertificate` array - |result| match result { - Ok((response, _connection)) => match response { - Response::Success(success) => match success.mime().unwrap().as_str() { - "text/gemini" => todo!(), + None, // optional `GTlsCertificate` + |result: Result| match result { + Ok(response) => { + match response.meta.status { + // route by status code + Status::Success => match response.meta.mime { + // handle `GIOStream` by content type + Some(Mime::TextGemini) => todo!(), + _ => todo!(), + }, _ => todo!(), - }, - _ => todo!(), - }, - Err(_) => todo!(), + } + } + Err(e) => todo!("{e}"), }, ); ExitCode::SUCCESS } ``` -## Other crates +## See also * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index 2152781..77026e9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,15 +4,15 @@ pub mod connection; pub mod error; -pub use connection::{Connection, Request, Response}; +pub use connection::Connection; pub use error::Error; -use gio::{Cancellable, SocketClient, SocketProtocol, TlsCertificate, prelude::SocketClientExt}; -use glib::Priority; +use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate}; +use glib::{Priority, Uri}; // Defaults -pub const DEFAULT_TIMEOUT: u32 = 30; +pub const DEFAULT_TIMEOUT: u32 = 10; pub const DEFAULT_SESSION_RESUMPTION: bool = false; /// Main point where connect external crate @@ -56,47 +56,41 @@ impl Client { /// * compatible with user (certificate) and guest (certificate-less) connection types pub fn request_async( &self, - request: Request, + uri: Uri, priority: Priority, cancellable: Cancellable, - client_certificate: Option, - server_certificates: Option>, - callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static, + certificate: Option, + callback: impl Fn(Result) + 'static, ) { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) - match request.to_network_address(crate::DEFAULT_PORT) { + match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { Ok(network_address) => { self.socket .connect_async(&network_address.clone(), Some(&cancellable.clone()), { let is_session_resumption = self.is_session_resumption; move |result| match result { Ok(socket_connection) => { - match Connection::build( - socket_connection.clone(), - network_address, - client_certificate, - server_certificates, + match Connection::new( + socket_connection, + certificate, + Some(network_address), is_session_resumption, ) { - Ok(connection) => connection.clone().request_async( - request, + Ok(connection) => connection.request_async( + uri.to_string(), priority, cancellable, - move |result| { - callback(match result { - Ok(response) => Ok(response), - Err(e) => Err(Error::Request(connection, e)), - }) + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), }, ), - Err(e) => { - callback(Err(Error::Connection(socket_connection, e))) - } + Err(e) => callback(Err(Error::Connection(e))), } } - Err(e) => callback(Err(Error::Connect(network_address, e))), + Err(e) => callback(Err(Error::Connect(e))), } }) } diff --git a/src/client/connection.rs b/src/client/connection.rs index 6be90f1..d5588bf 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,24 +1,19 @@ pub mod error; -pub mod request; pub mod response; pub use error::Error; -pub use request::{Mode, Request}; pub use response::Response; use gio::{ + prelude::{IOStreamExt, OutputStreamExt, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, - prelude::{IOStreamExt, OutputStreamExtManual, TlsCertificateExt, TlsConnectionExt}, }; use glib::{ - Bytes, Priority, object::{Cast, ObjectExt}, + Bytes, Priority, }; -#[derive(Debug, Clone)] pub struct Connection { - pub network_address: NetworkAddress, - pub socket_connection: SocketConnection, pub tls_client_connection: TlsClientConnection, } @@ -26,96 +21,58 @@ impl Connection { // Constructors /// Create new `Self` - pub fn build( + pub fn new( socket_connection: SocketConnection, - network_address: NetworkAddress, - client_certificate: Option, - server_certificates: Option>, + certificate: Option, + server_identity: Option, is_session_resumption: bool, ) -> Result { Ok(Self { tls_client_connection: match new_tls_client_connection( &socket_connection, - Some(&network_address), - server_certificates, + server_identity.as_ref(), is_session_resumption, ) { Ok(tls_client_connection) => { - if let Some(ref c) = client_certificate { - tls_client_connection.set_certificate(c); + if let Some(ref certificate) = certificate { + tls_client_connection.set_certificate(certificate); } tls_client_connection } Err(e) => return Err(e), }, - network_address, - socket_connection, }) } // Actions - /// Send new `Request` to `Self` connection using - /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or - /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol + /// Make new request to `Self` connection + /// * callback with new `Response` on success or `Error` on failure pub fn request_async( self, - request: Request, + query: String, priority: Priority, cancellable: Cancellable, - callback: impl FnOnce(Result<(Response, Self), Error>) + 'static, + callback: impl Fn(Result) + 'static, ) { - let output_stream = self.stream().output_stream(); - // Make sure **all header 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 - output_stream.clone().write_all_async( - Bytes::from_owned(request.header()), + // Send request + self.stream().output_stream().write_bytes_async( + &Bytes::from(format!("{query}\r\n").as_bytes()), priority, Some(&cancellable.clone()), move |result| match result { - Ok(_) => match request { - 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, mode, .. } => output_stream.write_all_async( - data, - priority, - Some(&cancellable.clone()), - move |result| match result { - 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))), - }, - ), - }, - Err((b, e)) => callback(Err(Error::Request(b, e))), + Ok(_) => { + // Read response + Response::from_connection_async(self, priority, cancellable, move |result| { + callback(match result { + Ok(response) => Ok(response), + Err(e) => Err(Error::Response(e)), + }) + }) + } + Err(e) => callback(Err(Error::Stream(e))), }, - ) + ); } // Getters @@ -129,15 +86,14 @@ impl Connection { } } -// Tools +// Helpers /// 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) -fn new_tls_client_connection( +/// using `server_identity` as [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) +pub 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) { @@ -145,25 +101,13 @@ fn new_tls_client_connection( // Prevent session resumption (certificate change ability in runtime) tls_client_connection.set_property("session-resumption-enabled", is_session_resumption); - // Return `Err` on server connection mismatch following specification lines: - // > Gemini servers MUST use the TLS close_notify implementation to close the connection - // > A client SHOULD notify the user of such a case + // @TODO handle // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections tls_client_connection.set_require_close_notify(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 - }) - }); + // @TODO validate + // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation + tls_client_connection.connect_accept_certificate(|_, _, _| true); Ok(tls_client_connection) } diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 711c2b6..2948f0f 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,16 +2,16 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Request(glib::Bytes, glib::Error), Response(crate::client::connection::response::Error), + Stream(glib::Error), TlsClientConnection(glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Request(_, e) => { - write!(f, "Request error: {e}") + Self::Stream(e) => { + write!(f, "TLS client connection error: {e}") } Self::Response(e) => { write!(f, "Response error: {e}") diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs deleted file mode 100644 index f67238b..0000000 --- a/src/client/connection/request.rs +++ /dev/null @@ -1,122 +0,0 @@ -pub mod error; -pub mod mode; - -pub use error::Error; -pub use mode::Mode; - -// Local dependencies - -use gio::NetworkAddress; -use glib::{Bytes, Uri, UriHideFlags}; - -/// Single `Request` implementation for different protocols -pub enum Request { - Gemini { - uri: Uri, - mode: Mode, - }, - Titan { - uri: Uri, - data: Bytes, - /// MIME type is optional attribute by Titan protocol specification, - /// but server MAY reject the request without `mime` value provided. - mime: Option, - token: Option, - mode: Mode, - }, -} - -impl Request { - // Getters - - /// Generate header string for `Self` - pub fn header(&self) -> String { - match self { - 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(mime) = mime { - header.push_str(&format!(";mime={mime}")); - } - if let Some(token) = token { - header.push_str(&format!(";token={token}")); - } - if let Some(query) = uri.query() { - header.push_str(&format!("?{query}")); - } - header.push_str("\r\n"); - header - } - } - } - - /// 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::Titan { uri, .. } => uri, - } - } - - /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self` - pub fn to_network_address(&self, default_port: u16) -> Result { - match crate::gio::network_address::from_uri(self.uri(), default_port) { - Ok(network_address) => Ok(network_address), - Err(e) => Err(Error::NetworkAddress(e)), - } - } -} - -#[test] -fn test_gemini_header() { - use glib::UriFlags; - - const REQUEST: &str = "gemini://geminiprotocol.net/"; - - assert_eq!( - Request::Gemini { - uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(), - mode: Mode::HeaderOnly - } - .header(), - format!("{REQUEST}\r\n") - ); -} - -#[test] -fn test_titan_header() { - use glib::UriFlags; - - const DATA: &[u8] = &[1, 2, 3]; - const MIME: &str = "plain/text"; - const TOKEN: &str = "token"; - - assert_eq!( - Request::Titan { - uri: Uri::parse( - "titan://geminiprotocol.net/raw/path?key=value", - UriFlags::NONE - ) - .unwrap(), - data: Bytes::from(DATA), - mime: Some(MIME.to_string()), - token: Some(TOKEN.to_string()), - mode: Mode::HeaderOnly - } - .header(), - format!( - "titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n", - DATA.len(), - ) - ); -} diff --git a/src/client/connection/request/error.rs b/src/client/connection/request/error.rs deleted file mode 100644 index 5524960..0000000 --- a/src/client/connection/request/error.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - NetworkAddress(crate::gio::network_address::error::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::NetworkAddress(e) => { - write!(f, "Network Address error: {e}") - } - } - } -} diff --git a/src/client/connection/request/mode.rs b/src/client/connection/request/mode.rs deleted file mode 100644 index b1d8a67..0000000 --- a/src/client/connection/request/mode.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// 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 7dd54bb..3ede7d0 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -1,146 +1,37 @@ -pub mod certificate; -pub mod error; -pub mod failure; -pub mod input; -pub mod redirect; -pub mod success; +//! Read and parse Gemini response as Object -pub use certificate::Certificate; -pub use error::{Error, HeaderBytesError}; -pub use failure::Failure; -pub use input::Input; -pub use redirect::Redirect; -pub use success::Success; +pub mod data; +pub mod error; +pub mod meta; + +pub use error::Error; +pub use meta::Meta; use super::Connection; -use gio::{Cancellable, IOStream}; -use glib::{Priority, object::IsA}; +use gio::Cancellable; +use glib::Priority; -const HEADER_LEN: usize = 1024; - -/// https://geminiprotocol.net/docs/protocol-specification.gmi#responses -pub enum Response { - Input(Input), // 1* - Success(Success), // 2* - Redirect(Redirect), // 3* - Failure(Failure), // 4*,5* - Certificate(Certificate), // 6* +pub struct Response { + pub connection: Connection, + pub meta: Meta, } impl Response { - /// Asynchronously create new `Self` for given `Connection` - pub fn header_from_connection_async( + // Constructors + + /// Create new `Self` from given `Connection` + /// * useful for manual [IOStream](https://docs.gtk.org/gio/class.IOStream.html) handle (based on `Meta` bytes pre-parsed) + pub fn from_connection_async( connection: Connection, priority: Priority, cancellable: Cancellable, - callback: impl FnOnce(Result, Connection) + 'static, + callback: impl FnOnce(Result) + 'static, ) { - header_from_stream_async( - Vec::with_capacity(HEADER_LEN), - connection.stream(), - cancellable, - priority, - |result| { - callback( - match result { - Ok(buffer) => match buffer.first() { - Some(b) => match b { - b'1' => match Input::from_utf8(&buffer) { - Ok(input) => Ok(Self::Input(input)), - Err(e) => Err(Error::Input(e)), - }, - b'2' => match Success::from_utf8(&buffer) { - Ok(success) => Ok(Self::Success(success)), - Err(e) => Err(Error::Success(e)), - }, - b'3' => match Redirect::from_utf8(&buffer) { - Ok(redirect) => Ok(Self::Redirect(redirect)), - Err(e) => Err(Error::Redirect(e)), - }, - b'4' | b'5' => match Failure::from_utf8(&buffer) { - Ok(failure) => Ok(Self::Failure(failure)), - Err(e) => Err(Error::Failure(e)), - }, - b'6' => match Certificate::from_utf8(&buffer) { - Ok(certificate) => Ok(Self::Certificate(certificate)), - Err(e) => Err(Error::Certificate(e)), - }, - b => Err(Error::Code(*b)), - }, - None => Err(Error::Protocol(buffer)), - }, - Err(e) => Err(e), - }, - connection, - ) - }, - ) + Meta::from_stream_async(connection.stream(), priority, cancellable, |result| { + callback(match result { + Ok(meta) => Ok(Self { connection, meta }), + Err(e) => Err(Error::Meta(e)), + }) + }) } } - -// Tools - -/// Asynchronously read header bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) -/// -/// Return UTF-8 buffer collected -/// * requires `IOStream` reference to keep `Connection` active in async thread -fn header_from_stream_async( - mut buffer: Vec, - stream: impl IsA, - cancellable: Cancellable, - priority: Priority, - 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((bytes, size)) => { - if size == 0 { - return callback(Ok(buffer)); - } - if buffer.len() + bytes.len() > HEADER_LEN { - buffer.extend(bytes); - return callback(Err(Error::Protocol(buffer))); - } - if bytes[0] == b'\r' { - buffer.extend(bytes); - return header_from_stream_async( - buffer, - stream, - cancellable, - priority, - callback, - ); - } - 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)) => 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 deleted file mode 100644 index 07e2891..0000000 --- a/src/client/connection/response/certificate.rs +++ /dev/null @@ -1,111 +0,0 @@ -pub mod error; -pub mod not_authorized; -pub mod not_valid; -pub mod required; - -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(Required), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - NotAuthorized(NotAuthorized), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - NotValid(NotValid), -} - -impl Certificate { - // Constructors - - /// Create new `Self` from buffer include header bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - 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 - - /// 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(required) => required.message(), - Self::NotAuthorized(not_authorized) => not_authorized.message(), - Self::NotValid(not_valid) => not_valid.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::Required(required) => required.message_or_default(), - Self::NotAuthorized(not_authorized) => not_authorized.message_or_default(), - Self::NotValid(not_valid) => not_valid.message_or_default(), - } - } - - /// 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(), - } - } - - /// 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() { - 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 deleted file mode 100644 index a710617..0000000 --- a/src/client/connection/response/certificate/error.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - 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::FirstByte(b) => { - write!(f, "Unexpected first byte: {b}") - } - 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 deleted file mode 100644 index b10d6ff..0000000 --- a/src/client/connection/response/certificate/not_authorized.rs +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/certificate/not_authorized/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 94e847c..0000000 --- a/src/client/connection/response/certificate/not_valid.rs +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/certificate/not_valid/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index b44585c..0000000 --- a/src/client/connection/response/certificate/required.rs +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/certificate/required/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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/data.rs b/src/client/connection/response/data.rs new file mode 100644 index 0000000..0009fe8 --- /dev/null +++ b/src/client/connection/response/data.rs @@ -0,0 +1,7 @@ +//! Gemini response could have different MIME type for data. +//! Use one of components below to parse response according to content type expected. +//! +//! * MIME type could be detected using `client::response::Meta` parser + +pub mod text; +pub use text::Text; diff --git a/src/client/connection/response/data/text.rs b/src/client/connection/response/data/text.rs new file mode 100644 index 0000000..2d3cf2c --- /dev/null +++ b/src/client/connection/response/data/text.rs @@ -0,0 +1,111 @@ +//! Tools for Text-based response + +pub mod error; +pub use error::Error; + +// Local dependencies +use gio::{ + prelude::{IOStreamExt, InputStreamExt}, + Cancellable, IOStream, +}; +use glib::{object::IsA, GString, Priority}; + +// Default limits +pub const BUFFER_CAPACITY: usize = 0x400; // 1024 +pub const BUFFER_MAX_SIZE: usize = 0xfffff; // 1M + +/// Container for text-based response data +pub struct Text { + pub data: GString, +} + +impl Default for Text { + fn default() -> Self { + Self::new() + } +} + +impl Text { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + Self { + data: GString::new(), + } + } + + /// Create new `Self` from string + pub fn from_string(data: &str) -> Self { + Self { data: data.into() } + } + + /// Create new `Self` from UTF-8 buffer + pub fn from_utf8(buffer: &[u8]) -> Result { + match GString::from_utf8(buffer.into()) { + Ok(data) => Ok(Self::from_string(&data)), + Err(e) => Err(Error::Decode(e)), + } + } + + /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + pub fn from_stream_async( + stream: impl IsA, + priority: Priority, + cancellable: Cancellable, + on_complete: impl FnOnce(Result) + 'static, + ) { + read_all_from_stream_async( + Vec::with_capacity(BUFFER_CAPACITY), + stream, + cancellable, + priority, + |result| match result { + Ok(buffer) => on_complete(Self::from_utf8(&buffer)), + Err(e) => on_complete(Err(e)), + }, + ); + } +} + +// Tools + +/// Asynchronously read all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// +/// Return UTF-8 buffer collected +/// * require `IOStream` reference to keep `Connection` active in async thread +pub fn read_all_from_stream_async( + mut buffer: Vec, + stream: impl IsA, + cancelable: Cancellable, + priority: Priority, + callback: impl FnOnce(Result, Error>) + 'static, +) { + stream.input_stream().read_bytes_async( + BUFFER_CAPACITY, + priority, + Some(&cancelable.clone()), + move |result| match result { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return callback(Ok(buffer)); + } + + // Validate overflow + if buffer.len() + bytes.len() > BUFFER_MAX_SIZE { + return callback(Err(Error::BufferOverflow)); + } + + // Save chunks to buffer + for &byte in bytes.iter() { + buffer.push(byte); + } + + // Continue bytes reading + read_all_from_stream_async(buffer, stream, cancelable, priority, callback); + } + Err(e) => callback(Err(Error::InputStream(e))), + }, + ); +} diff --git a/src/client/connection/response/data/text/error.rs b/src/client/connection/response/data/text/error.rs new file mode 100644 index 0000000..4a853aa --- /dev/null +++ b/src/client/connection/response/data/text/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + BufferOverflow, + Decode(std::string::FromUtf8Error), + InputStream(glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::BufferOverflow => { + write!(f, "Buffer overflow") + } + Self::Decode(e) => { + write!(f, "Decode error: {e}") + } + Self::InputStream(e) => { + write!(f, "Input stream read error: {e}") + } + } + } +} diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index 022ed62..9834190 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -1,69 +1,19 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Certificate(super::certificate::Error), - Code(u8), - Failure(super::failure::Error), - Input(super::input::Error), - Protocol(Vec), - Redirect(super::redirect::Error), - Stream(glib::Error, Vec), - Success(super::success::Error), - Utf8Error(Utf8Error), + Meta(super::meta::Error), + Stream, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Certificate(e) => { - write!(f, "Certificate error: {e}") + Self::Meta(e) => { + write!(f, "Meta read error: {e}") } - Self::Code(b) => { - write!(f, "Unexpected status code byte: {b}") - } - Self::Failure(e) => { - write!(f, "Failure error: {e}") - } - Self::Input(e) => { - write!(f, "Input error: {e}") - } - Self::Protocol(..) => { - write!(f, "Protocol error") - } - Self::Redirect(e) => { - write!(f, "Redirect error: {e}") - } - Self::Stream(e, ..) => { - write!(f, "I/O stream error: {e}") - } - Self::Success(e) => { - write!(f, "Success error: {e}") - } - Self::Utf8Error(e) => { - write!(f, "UTF-8 decode error: {e}") - } - } - } -} - -#[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") + Self::Stream => { + write!(f, "I/O stream error") } } } diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs deleted file mode 100644 index 1ace0ed..0000000 --- a/src/client/connection/response/failure.rs +++ /dev/null @@ -1,89 +0,0 @@ -pub mod error; -pub mod permanent; -pub mod temporary; - -pub use error::Error; -pub use permanent::Permanent; -pub use temporary::Temporary; - -pub enum Failure { - /// 4* status code group - /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure - Temporary(Temporary), - /// 5* status code group - /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure - Permanent(Permanent), -} - -impl Failure { - // Constructors - - /// Create new `Self` from buffer include header bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - match buffer.first() { - Some(b) => match b { - b'4' => match Temporary::from_utf8(buffer) { - Ok(input) => Ok(Self::Temporary(input)), - Err(e) => Err(Error::Temporary(e)), - }, - b'5' => match Permanent::from_utf8(buffer) { - Ok(failure) => Ok(Self::Permanent(failure)), - Err(e) => Err(Error::Permanent(e)), - }, - b => Err(Error::Code(*b)), - }, - None => Err(Error::Protocol), - } - } - - // Getters - - /// 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 deleted file mode 100644 index 056f714..0000000 --- a/src/client/connection/response/failure/error.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Code(u8), - Permanent(super::permanent::Error), - Protocol, - Temporary(super::temporary::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::Code(b) => { - write!(f, "Unexpected status code byte: {b}") - } - Self::Permanent(e) => { - write!(f, "Permanent failure group error: {e}") - } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Temporary(e) => { - write!(f, "Temporary failure group error: {e}") - } - } - } -} diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs deleted file mode 100644 index 526a208..0000000 --- a/src/client/connection/response/failure/permanent.rs +++ /dev/null @@ -1,126 +0,0 @@ -pub mod bad_request; -pub mod default; -pub mod error; -pub mod gone; -pub mod not_found; -pub mod proxy_request_refused; - -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(Default), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found - NotFound(NotFound), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone - Gone(Gone), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused - ProxyRequestRefused(ProxyRequestRefused), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request - BadRequest(BadRequest), -} - -impl Permanent { - // Constructors - - /// Create new `Self` from buffer include header bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - 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 message(&self) -> Option<&str> { - match self { - 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(), - } - } - - /// 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() - } - Self::BadRequest(bad_request) => bad_request.message_or_default(), - } - } - - /// 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(), - } - } - - /// 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() { - 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 deleted file mode 100644 index 8cfa6f9..0000000 --- a/src/client/connection/response/failure/permanent/bad_request.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/permanent/bad_request/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 466333d..0000000 --- a/src/client/connection/response/failure/permanent/default.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/permanent/default/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index df334f5..0000000 --- a/src/client/connection/response/failure/permanent/error.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - 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::BadRequest(e) => { - write!(f, "BadRequest parse 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 deleted file mode 100644 index f93d068..0000000 --- a/src/client/connection/response/failure/permanent/gone.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/permanent/gone/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index d5ddca9..0000000 --- a/src/client/connection/response/failure/permanent/not_found.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/permanent/not_found/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index fba229c..0000000 --- a/src/client/connection/response/failure/permanent/proxy_request_refused.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index cc20834..0000000 --- a/src/client/connection/response/failure/temporary.rs +++ /dev/null @@ -1,126 +0,0 @@ -pub mod cgi_error; -pub mod default; -pub mod error; -pub mod proxy_error; -pub mod server_unavailable; -pub mod 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(Default), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable(ServerUnavailable), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError(CgiError), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError(ProxyError), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown(SlowDown), -} - -impl Temporary { - // Constructors - - /// Create new `Self` from buffer include header bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - 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 message(&self) -> Option<&str> { - match self { - 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(), - } - } - - /// 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(), - } - } - - /// 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(), - } - } - - /// 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() { - 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 deleted file mode 100644 index 8843fa9..0000000 --- a/src/client/connection/response/failure/temporary/cgi_error.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/temporary/cgi_error/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index e56d90b..0000000 --- a/src/client/connection/response/failure/temporary/default.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/temporary/default/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index afa9154..0000000 --- a/src/client/connection/response/failure/temporary/error.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - 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::CgiError(e) => { - write!(f, "CgiError parse 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 deleted file mode 100644 index 1264c34..0000000 --- a/src/client/connection/response/failure/temporary/proxy_error.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/temporary/proxy_error/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index f42802e..0000000 --- a/src/client/connection/response/failure/temporary/server_unavailable.rs +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/temporary/server_unavailable/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 3ca346d..0000000 --- a/src/client/connection/response/failure/temporary/slow_down.rs +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/failure/temporary/slow_down/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 0cd9857..0000000 --- a/src/client/connection/response/input.rs +++ /dev/null @@ -1,92 +0,0 @@ -pub mod default; -pub mod error; -pub mod sensitive; - -pub use default::Default; -pub use error::Error; -pub use sensitive::Sensitive; - -const CODE: u8 = b'1'; - -/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected) -pub enum Input { - Default(Default), - Sensitive(Sensitive), -} - -impl Input { - // Constructors - - /// Create new `Self` from buffer include header bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - 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 message(&self) -> Option<&str> { - match self { - Self::Default(default) => default.message(), - Self::Sensitive(sensitive) => sensitive.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::Default(default) => default.message_or_default(), - Self::Sensitive(sensitive) => sensitive.message_or_default(), - } - } - - /// 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(), - } - } - - /// 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() { - 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 - t("10 Default\r\n", Some("Default")); - t("10\r\n", None); - // 11 - 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 deleted file mode 100644 index 4a5a3df..0000000 --- a/src/client/connection/response/input/default.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/input/default/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 6763727..0000000 --- a/src/client/connection/response/input/error.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - 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::Default(e) => { - write!(f, "Default parse error: {e}") - } - 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 deleted file mode 100644 index 594c8fb..0000000 --- a/src/client/connection/response/input/sensitive.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4f0ee59..0000000 --- a/src/client/connection/response/input/sensitive/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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/meta.rs b/src/client/connection/response/meta.rs new file mode 100644 index 0000000..26388ad --- /dev/null +++ b/src/client/connection/response/meta.rs @@ -0,0 +1,147 @@ +//! Components for reading and parsing meta bytes from response: +//! * [Gemini status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) +//! * meta data (for interactive statuses like 10, 11, 30 etc) +//! * MIME type + +pub mod data; +pub mod error; +pub mod mime; +pub mod status; + +pub use data::Data; +pub use error::Error; +pub use mime::Mime; +pub use status::Status; + +use gio::{ + prelude::{IOStreamExt, InputStreamExtManual}, + Cancellable, IOStream, +}; +use glib::{object::IsA, Priority}; + +pub const MAX_LEN: usize = 0x400; // 1024 + +pub struct Meta { + pub status: Status, + pub data: Option, + pub mime: Option, + // @TODO + // charset: Option, + // language: Option, +} + +impl Meta { + // Constructors + + /// Create new `Self` from UTF-8 buffer + /// * supports entire response or just meta slice + pub fn from_utf8(buffer: &[u8]) -> Result { + // Calculate buffer length once + let len = buffer.len(); + + // Parse meta bytes only + match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { + Some(slice) => { + // Parse data + let data = Data::from_utf8(slice); + + if let Err(e) = data { + return Err(Error::Data(e)); + } + + // MIME + + let mime = Mime::from_utf8(slice); + + if let Err(e) = mime { + return Err(Error::Mime(e)); + } + + // Status + + let status = Status::from_utf8(slice); + + if let Err(e) = status { + return Err(Error::Status(e)); + } + + Ok(Self { + data: data.unwrap(), + mime: mime.unwrap(), + status: status.unwrap(), + }) + } + None => Err(Error::Protocol), + } + } + + /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + pub fn from_stream_async( + stream: impl IsA, + priority: Priority, + cancellable: Cancellable, + on_complete: impl FnOnce(Result) + 'static, + ) { + read_from_stream_async( + Vec::with_capacity(MAX_LEN), + stream, + cancellable, + priority, + |result| match result { + Ok(buffer) => on_complete(Self::from_utf8(&buffer)), + Err(e) => on_complete(Err(e)), + }, + ); + } +} + +// Tools + +/// Asynchronously read all meta bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// +/// Return UTF-8 buffer collected +/// * require `IOStream` reference to keep `Connection` active in async thread +pub fn read_from_stream_async( + mut buffer: Vec, + stream: impl IsA, + cancellable: Cancellable, + priority: Priority, + on_complete: impl FnOnce(Result, Error>) + 'static, +) { + stream.input_stream().read_async( + vec![0], + priority, + Some(&cancellable.clone()), + move |result| match result { + Ok((mut bytes, size)) => { + // Expect valid header length + if size == 0 || buffer.len() >= MAX_LEN { + return on_complete(Err(Error::Protocol)); + } + + // Read next byte without record + if bytes.contains(&b'\r') { + return read_from_stream_async( + buffer, + stream, + cancellable, + priority, + on_complete, + ); + } + + // Complete without record + if bytes.contains(&b'\n') { + return on_complete(Ok(buffer)); + } + + // Record + buffer.append(&mut bytes); + + // Continue + read_from_stream_async(buffer, stream, cancellable, priority, on_complete); + } + Err((data, e)) => on_complete(Err(Error::InputStream(data, e))), + }, + ); +} diff --git a/src/client/connection/response/meta/charset.rs b/src/client/connection/response/meta/charset.rs new file mode 100644 index 0000000..1673a59 --- /dev/null +++ b/src/client/connection/response/meta/charset.rs @@ -0,0 +1 @@ +// @TODO diff --git a/src/client/connection/response/meta/data.rs b/src/client/connection/response/meta/data.rs new file mode 100644 index 0000000..e86af61 --- /dev/null +++ b/src/client/connection/response/meta/data.rs @@ -0,0 +1,61 @@ +//! Components for reading and parsing meta **data** bytes from response +//! (e.g. placeholder text for 10, 11, url string for 30, 31 etc) + +pub mod error; +pub use error::Error; + +use glib::GString; + +/// Meta **data** holder +/// +/// For example, `value` could contain: +/// * placeholder text for 10, 11 status +/// * URL string for 30, 31 status +pub struct Data { + pub value: GString, +} + +impl Data { + // Constructors + + /// Parse meta **data** from UTF-8 buffer + /// from entire response or just header slice + /// + /// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) + /// that does not expect any data in header + pub fn from_utf8(buffer: &[u8]) -> Result, Error> { + // Define max buffer length for this method + const MAX_LEN: usize = 0x400; // 1024 + + // Init bytes buffer + let mut bytes: Vec = Vec::with_capacity(MAX_LEN); + + // Calculate len once + let len = buffer.len(); + + // Skip 3 bytes for status code of `MAX_LEN` expected + match buffer.get(3..if len > MAX_LEN { MAX_LEN - 3 } else { len }) { + Some(slice) => { + for &byte in slice { + // End of header + if byte == b'\r' { + break; + } + + // Continue + bytes.push(byte); + } + + // Assumes the bytes are valid UTF-8 + match GString::from_utf8(bytes) { + Ok(value) => Ok(match value.is_empty() { + false => Some(Self { value }), + true => None, + }), + Err(e) => Err(Error::Decode(e)), + } + } + None => Err(Error::Protocol), + } + } +} diff --git a/src/client/connection/response/success/default/error.rs b/src/client/connection/response/meta/data/error.rs similarity index 51% rename from src/client/connection/response/success/default/error.rs rename to src/client/connection/response/meta/data/error.rs index d5b28b5..49455cd 100644 --- a/src/client/connection/response/success/default/error.rs +++ b/src/client/connection/response/meta/data/error.rs @@ -2,18 +2,18 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Header(super::header::Error), + Decode(std::string::FromUtf8Error), + Protocol, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Unexpected status code") + Self::Decode(e) => { + write!(f, "Decode error: {e}") } - Self::Header(e) => { - write!(f, "Header error: {e}") + Self::Protocol => { + write!(f, "Protocol error") } } } diff --git a/src/client/connection/response/meta/error.rs b/src/client/connection/response/meta/error.rs new file mode 100644 index 0000000..55abb24 --- /dev/null +++ b/src/client/connection/response/meta/error.rs @@ -0,0 +1,33 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Data(super::data::Error), + InputStream(Vec, glib::Error), + Mime(super::mime::Error), + Protocol, + Status(super::status::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Data(e) => { + write!(f, "Data error: {e}") + } + Self::InputStream(_, e) => { + // @TODO + write!(f, "Input stream error: {e}") + } + Self::Mime(e) => { + write!(f, "MIME error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Status(e) => { + write!(f, "Status error: {e}") + } + } + } +} diff --git a/src/client/connection/response/meta/language.rs b/src/client/connection/response/meta/language.rs new file mode 100644 index 0000000..1673a59 --- /dev/null +++ b/src/client/connection/response/meta/language.rs @@ -0,0 +1 @@ +// @TODO diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs new file mode 100644 index 0000000..c86d627 --- /dev/null +++ b/src/client/connection/response/meta/mime.rs @@ -0,0 +1,142 @@ +//! MIME type parser for different data types: +//! +//! * UTF-8 buffer with entire response or just with meta slice (that include entire **header**) +//! * String (that include **header**) +//! * [Uri](https://docs.gtk.org/glib/struct.Uri.html) (that include **extension**) +//! * `std::Path` (that include **extension**) + +pub mod error; +pub use error::Error; + +use glib::{GString, Uri}; +use std::path::Path; + +/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters +#[derive(Debug)] +pub enum Mime { + // Text + TextGemini, + TextPlain, + // Image + ImageGif, + ImageJpeg, + ImagePng, + ImageSvg, + ImageWebp, + // Audio + AudioFlac, + AudioMpeg, + AudioOgg, +} // @TODO + +impl Mime { + /// Create new `Self` from UTF-8 buffer (that includes **header**) + /// + /// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) + /// that does not expect MIME type in header + /// * includes `Self::from_string` parser, + /// it means that given buffer should contain some **header** (not filepath or any other type of strings) + pub fn from_utf8(buffer: &[u8]) -> Result, Error> { + // Define max buffer length for this method + const MAX_LEN: usize = 0x400; // 1024 + + // Calculate buffer length once + let len = buffer.len(); + + // Parse meta bytes only + match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { + Some(value) => match GString::from_utf8(value.into()) { + Ok(string) => Self::from_string(string.as_str()), + Err(e) => Err(Error::Decode(e)), + }, + None => Err(Error::Protocol), + } + } + + /// Create new `Self` from `std::Path` that includes file **extension** + pub fn from_path(path: &Path) -> Result { + match path.extension().and_then(|extension| extension.to_str()) { + // Text + Some("gmi" | "gemini") => Ok(Self::TextGemini), + Some("txt") => Ok(Self::TextPlain), + + // Image + Some("gif") => Ok(Self::ImageGif), + Some("jpeg" | "jpg") => Ok(Self::ImageJpeg), + Some("png") => Ok(Self::ImagePng), + Some("svg") => Ok(Self::ImageSvg), + Some("webp") => Ok(Self::ImageWebp), + + // Audio + Some("flac") => Ok(Self::AudioFlac), + Some("mp3") => Ok(Self::AudioMpeg), + Some("oga" | "ogg" | "opus" | "spx") => Ok(Self::AudioOgg), + _ => Err(Error::Undefined), + } // @TODO extension to lowercase + } + + /// Create new `Self` from string that includes **header** + /// + /// **Return** + /// + /// * `None` if MIME type not found + /// * `Error::Undefined` if status code 2* and type not found in `Mime` enum + pub fn from_string(value: &str) -> Result, Error> { + // Text + if value.contains("text/gemini") { + return Ok(Some(Self::TextGemini)); + } + + if value.contains("text/plain") { + return Ok(Some(Self::TextPlain)); + } + + // Image + if value.contains("image/gif") { + return Ok(Some(Self::ImageGif)); + } + + if value.contains("image/jpeg") { + return Ok(Some(Self::ImageJpeg)); + } + + if value.contains("image/png") { + return Ok(Some(Self::ImagePng)); + } + + if value.contains("image/svg+xml") { + return Ok(Some(Self::ImageSvg)); + } + + if value.contains("image/webp") { + return Ok(Some(Self::ImageWebp)); + } + + // Audio + if value.contains("audio/flac") { + return Ok(Some(Self::AudioFlac)); + } + + if value.contains("audio/mpeg") { + return Ok(Some(Self::AudioMpeg)); + } + + if value.contains("audio/ogg") { + return Ok(Some(Self::AudioOgg)); + } + + // Some type exist, but not defined yet (on status code is 2*) + if value.starts_with("2") && value.contains("/") { + return Err(Error::Undefined); + } + + // Done + Ok(None) // may be empty (status code ^2*) + } + + /// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html) + /// that includes file **extension** + pub fn from_uri(uri: &Uri) -> Result { + Self::from_path(Path::new(&uri.to_string())) + } +} diff --git a/src/client/connection/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs new file mode 100644 index 0000000..4b66ed2 --- /dev/null +++ b/src/client/connection/response/meta/mime/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Decode(std::string::FromUtf8Error), + Protocol, + Undefined, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Decode(e) => { + write!(f, "Decode error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Undefined => { + write!(f, "Undefined error") + } + } + } +} diff --git a/src/client/connection/response/meta/status.rs b/src/client/connection/response/meta/status.rs new file mode 100644 index 0000000..c0079f4 --- /dev/null +++ b/src/client/connection/response/meta/status.rs @@ -0,0 +1,82 @@ +//! Parser and holder tools for +//! [Status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) + +pub mod error; +pub use error::Error; + +use glib::GString; + +/// Holder for [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) +#[derive(Debug)] +pub enum Status { + // Input + Input = 10, + SensitiveInput = 11, + // Success + Success = 20, + // Redirect + Redirect = 30, + PermanentRedirect = 31, + // Temporary failure + TemporaryFailure = 40, + ServerUnavailable = 41, + CgiError = 42, + ProxyError = 43, + SlowDown = 44, + // Permanent failure + PermanentFailure = 50, + NotFound = 51, + ResourceGone = 52, + ProxyRequestRefused = 53, + BadRequest = 59, + // Client certificates + CertificateRequest = 60, + CertificateUnauthorized = 61, + CertificateInvalid = 62, +} + +impl Status { + /// Create new `Self` from UTF-8 buffer + /// + /// * includes `Self::from_string` parser, it means that given buffer should contain some **header** + pub fn from_utf8(buffer: &[u8]) -> Result { + match buffer.get(0..2) { + Some(value) => match GString::from_utf8(value.to_vec()) { + Ok(string) => Self::from_string(string.as_str()), + Err(e) => Err(Error::Decode(e)), + }, + None => Err(Error::Protocol), + } + } + + /// Create new `Self` from string that includes **header** + pub fn from_string(code: &str) -> Result { + match code { + // Input + "10" => Ok(Self::Input), + "11" => Ok(Self::SensitiveInput), + // Success + "20" => Ok(Self::Success), + // Redirect + "30" => Ok(Self::Redirect), + "31" => Ok(Self::PermanentRedirect), + // Temporary failure + "40" => Ok(Self::TemporaryFailure), + "41" => Ok(Self::ServerUnavailable), + "42" => Ok(Self::CgiError), + "43" => Ok(Self::ProxyError), + "44" => Ok(Self::SlowDown), + // Permanent failure + "50" => Ok(Self::PermanentFailure), + "51" => Ok(Self::NotFound), + "52" => Ok(Self::ResourceGone), + "53" => Ok(Self::ProxyRequestRefused), + "59" => Ok(Self::BadRequest), + // Client certificates + "60" => Ok(Self::CertificateRequest), + "61" => Ok(Self::CertificateUnauthorized), + "62" => Ok(Self::CertificateInvalid), + _ => Err(Error::Undefined), + } + } +} diff --git a/src/client/connection/response/meta/status/error.rs b/src/client/connection/response/meta/status/error.rs new file mode 100644 index 0000000..4b66ed2 --- /dev/null +++ b/src/client/connection/response/meta/status/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Decode(std::string::FromUtf8Error), + Protocol, + Undefined, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Decode(e) => { + write!(f, "Decode error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Undefined => { + write!(f, "Undefined error") + } + } + } +} diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs deleted file mode 100644 index 48bd610..0000000 --- a/src/client/connection/response/redirect.rs +++ /dev/null @@ -1,185 +0,0 @@ -pub mod error; -pub mod permanent; -pub mod temporary; - -pub use error::{Error, UriError}; -pub use permanent::Permanent; -pub use temporary::Temporary; - -// 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(Temporary), - /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Permanent(Permanent), -} - -impl Redirect { - // Constructors - - /// Create new `Self` from buffer include header bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - 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) -> Result<&str, Error> { - match self { - 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), - } - } -} - -// 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(), - } - }, - UriFlags::NONE, - ) { - Ok(absolute) => Ok(absolute), - Err(e) => Err(UriError::ParseRelative(e)), - } -} - -#[test] -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, - Some("geminiprotocol.net"), - -1, - "/path/", - Some("query"), - Some("fragment"), - ); - // codes test - t( - &base, - "30 gemini://geminiprotocol.net/path\r\n", - "gemini://geminiprotocol.net/path", - ); - t( - &base, - "31 gemini://geminiprotocol.net/path\r\n", - "gemini://geminiprotocol.net/path", - ); - // 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 deleted file mode 100644 index 38aaab1..0000000 --- a/src/client/connection/response/redirect/error.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - 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::FirstByte(b) => { - write!(f, "Unexpected first byte: {b}") - } - Self::Permanent(e) => { - write!(f, "Permanent parse error: {e}") - } - Self::SecondByte(b) => { - write!(f, "Unexpected second byte: {b}") - } - 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 deleted file mode 100644 index e8e6371..0000000 --- a/src/client/connection/response/redirect/permanent.rs +++ /dev/null @@ -1,82 +0,0 @@ -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 deleted file mode 100644 index 0d0e81d..0000000 --- a/src/client/connection/response/redirect/permanent/error.rs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index a131336..0000000 --- a/src/client/connection/response/redirect/temporary.rs +++ /dev/null @@ -1,82 +0,0 @@ -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 deleted file mode 100644 index 0d0e81d..0000000 --- a/src/client/connection/response/redirect/temporary/error.rs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index f9493d6..0000000 --- a/src/client/connection/response/success.rs +++ /dev/null @@ -1,76 +0,0 @@ -pub mod default; -pub mod error; - -pub use default::Default; -pub use error::Error; - -const CODE: u8 = b'2'; - -pub enum Success { - Default(Default), - // reserved for 2* codes -} - -impl Success { - // Constructors - - /// Parse new `Self` from buffer bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - if buffer.first().is_none_or(|b| *b != CODE) { - return Err(Error::Code); - } - match Default::from_utf8(buffer) { - Ok(default) => Ok(Self::Default(default)), - Err(e) => Err(Error::Default(e)), - } - } - - // Getters - - /// Get header bytes for `Self` type - pub fn as_header_bytes(&self) -> &[u8] { - match self { - Self::Default(default) => default.header.as_bytes(), - } - } - - /// Get header string for `Self` type - pub fn as_header_str(&self) -> &str { - match self { - Self::Default(default) => default.header.as_str(), - } - } - - /// 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() { - let r = "20 text/gemini; charset=utf-8; lang=en\r\n"; - let b = r.as_bytes(); - let s = Success::from_utf8(b).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!(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 deleted file mode 100644 index 488c3e6..0000000 --- a/src/client/connection/response/success/default.rs +++ /dev/null @@ -1,51 +0,0 @@ -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/header.rs b/src/client/connection/response/success/default/header.rs deleted file mode 100644 index dab58b7..0000000 --- a/src/client/connection/response/success/default/header.rs +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 4daca3a..0000000 --- a/src/client/connection/response/success/default/header/error.rs +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index fe32c5f..0000000 --- a/src/client/connection/response/success/error.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Code, - Default(super::default::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::Code => { - write!(f, "Unexpected status code") - } - Self::Default(e) => { - write!(f, "Header error: {e}") - } - } - } -} diff --git a/src/client/error.rs b/src/client/error.rs index 73031da..0ebb3c3 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -2,29 +2,26 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum 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, - ), + Connect(glib::Error), + Connection(crate::client::connection::Error), + NetworkAddress(crate::gio::network_address::Error), + Request(glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Connect(_, e) => { - write!(f, "Connect error: {e}") + Self::Connection(e) => { + write!(f, "Connection error: {e}") } - Self::Connection(_, e) => { - write!(f, "Connection init error: {e}") + Self::Connect(e) => { + write!(f, "Connect error: {e}") } Self::NetworkAddress(e) => { write!(f, "Network address error: {e}") } - Self::Request(_, e) => { - write!(f, "Connection error: {e}") + Self::Request(e) => { + write!(f, "Request error: {e}") } } } diff --git a/src/gio.rs b/src/gio.rs index 72a89d8..8206018 100644 --- a/src/gio.rs +++ b/src/gio.rs @@ -1,3 +1,2 @@ -pub mod file_output_stream; pub mod memory_input_stream; pub mod network_address; diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs deleted file mode 100644 index 2dffb5e..0000000 --- a/src/gio/file_output_stream.rs +++ /dev/null @@ -1,69 +0,0 @@ -pub mod error; -pub mod size; - -pub use error::Error; -pub use size::Size; - -use gio::{ - Cancellable, FileOutputStream, IOStream, - prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual}, -}; -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) -/// * require `IOStream` reference to keep `Connection` active in async thread -pub fn from_stream_async( - io_stream: impl IsA, - file_output_stream: FileOutputStream, - cancellable: Cancellable, - priority: Priority, - 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( - size.chunk, - priority, - Some(&cancellable.clone()), - move |result| match result { - Ok(bytes) => { - size.total += bytes.len(); - on_chunk(bytes.clone(), size.total); - - if let Some(limit) = size.limit - && size.total > limit - { - return on_complete(Err(Error::BytesTotal(size.total, limit))); - } - - 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, - priority, - Some(&cancellable.clone()), - 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/error.rs b/src/gio/file_output_stream/error.rs deleted file mode 100644 index 6b1ee76..0000000 --- a/src/gio/file_output_stream/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - BytesTotal(usize, usize), - InputStream(glib::Error), - OutputStream(glib::Bytes, glib::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::BytesTotal(total, limit) => { - write!(f, "Bytes total limit reached: {total} / {limit}") - } - Self::InputStream(e) => { - write!(f, "Input stream error: {e}") - } - Self::OutputStream(_, e) => { - write!(f, "Output stream error: {e}") - } - } - } -} diff --git a/src/gio/file_output_stream/size.rs b/src/gio/file_output_stream/size.rs deleted file mode 100644 index 5d0c911..0000000 --- a/src/gio/file_output_stream/size.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// 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 2b1fc39..dbb0bc3 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -1,14 +1,11 @@ pub mod error; -pub mod size; - pub use error::Error; -pub use size::Size; use gio::{ - Cancellable, IOStream, MemoryInputStream, prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, + Cancellable, IOStream, MemoryInputStream, }; -use glib::{Priority, object::IsA}; +use glib::{object::IsA, Bytes, Priority}; /// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) @@ -17,79 +14,79 @@ use glib::{Priority, object::IsA}; /// * safe read (of memory overflow) to dynamically allocated buffer, where final size of target data unknown /// * calculate bytes processed on chunk load pub fn from_stream_async( - io_stream: impl IsA, - priority: Priority, + base_io_stream: impl IsA, cancelable: Cancellable, - size: Size, - (on_chunk, on_complete): ( - impl Fn(usize, usize) + 'static, - impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, - ), + priority: Priority, + bytes_in_chunk: usize, + bytes_total_limit: usize, + on_chunk: impl Fn((Bytes, usize)) + 'static, + on_complete: impl FnOnce(Result) + 'static, ) { - for_memory_input_stream_async( + read_all_from_stream_async( MemoryInputStream::new(), - io_stream, - priority, + base_io_stream, cancelable, - size, + priority, + (bytes_in_chunk, bytes_total_limit, 0), (on_chunk, on_complete), ); } -/// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) -/// to [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) +/// Asynchronously read entire [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +/// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread -pub fn for_memory_input_stream_async( +pub fn read_all_from_stream_async( memory_input_stream: MemoryInputStream, - io_stream: impl IsA, + base_io_stream: impl IsA, + cancelable: Cancellable, priority: Priority, - cancellable: Cancellable, - mut size: Size, - (on_chunk, on_complete): ( - impl Fn(usize, usize) + 'static, - impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, + bytes: (usize, usize, usize), + callback: ( + impl Fn((Bytes, usize)) + 'static, + impl FnOnce(Result) + 'static, ), ) { - io_stream.input_stream().read_bytes_async( - size.chunk, + let (on_chunk, on_complete) = callback; + let (bytes_in_chunk, bytes_total_limit, bytes_total) = bytes; + + base_io_stream.input_stream().read_bytes_async( + bytes_in_chunk, priority, - Some(&cancellable.clone()), + Some(&cancelable.clone()), move |result| match result { Ok(bytes) => { - let len = bytes.len(); // calculate once + // Update bytes total + let bytes_total = bytes_total + bytes.len(); - // is end of stream - if len == 0 { - return on_complete(Ok((memory_input_stream, size.total))); + // Callback chunk function + on_chunk((bytes.clone(), bytes_total)); + + // Validate max size + if bytes_total > bytes_total_limit { + return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); } - // callback chunk function - size.total += len; - on_chunk(len, size.total); + // No bytes were read, end of stream + if bytes.len() == 0 { + return on_complete(Ok(memory_input_stream)); + } - // push bytes into the memory pool + // Write chunk bytes memory_input_stream.add_bytes(&bytes); - // prevent memory overflow - if size.total > size.limit { - return on_complete(Err(Error::BytesTotal( - memory_input_stream, - size.total, - size.limit, - ))); - } - - // handle next chunk.. - for_memory_input_stream_async( + // Continue + read_all_from_stream_async( memory_input_stream, - io_stream, + base_io_stream, + cancelable, priority, - cancellable, - size, + (bytes_in_chunk, bytes_total_limit, bytes_total), (on_chunk, on_complete), - ) + ); + } + Err(e) => { + on_complete(Err(Error::InputStream(e))); } - Err(e) => on_complete(Err(Error::InputStream(memory_input_stream, e))), }, - ) + ); } diff --git a/src/gio/memory_input_stream/error.rs b/src/gio/memory_input_stream/error.rs index 6b8ae86..673906c 100644 --- a/src/gio/memory_input_stream/error.rs +++ b/src/gio/memory_input_stream/error.rs @@ -2,17 +2,17 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - BytesTotal(gio::MemoryInputStream, usize, usize), - InputStream(gio::MemoryInputStream, glib::Error), + BytesTotal(usize, usize), + InputStream(glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::BytesTotal(_, total, limit) => { + Self::BytesTotal(total, limit) => { write!(f, "Bytes total limit reached: {total} / {limit}") } - Self::InputStream(_, e) => { + Self::InputStream(e) => { write!(f, "Input stream error: {e}") } } diff --git a/src/gio/memory_input_stream/size.rs b/src/gio/memory_input_stream/size.rs deleted file mode 100644 index 9a10bd3..0000000 --- a/src/gio/memory_input_stream/size.rs +++ /dev/null @@ -1,16 +0,0 @@ -/// 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, - } - } -} diff --git a/src/lib.rs b/src/lib.rs index afd549d..868a4f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,11 +8,3 @@ pub use client::Client; // Global defaults pub const DEFAULT_PORT: u16 = 1965; - -// Debug - -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); - -pub const VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR"); -pub const VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR"); -pub const VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH"); diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..1673a59 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1 @@ +// @TODO
-sudo apt install libglib2.0-dev
-sudo dnf install glib2-devel