From 3cde80b6a88ed61637c7e923e4baf4212f746f8a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:22:26 +0300 Subject: [PATCH] draft new api version --- README.md | 65 ++----- src/client.rs | 42 +---- src/client/buffer.rs | 164 ++++++++++++++++++ .../connection/input => }/buffer/error.rs | 2 +- src/client/response.rs | 16 +- src/client/response/body.rs | 12 +- src/client/response/header.rs | 60 ++++--- src/client/response/header/error.rs | 3 +- src/client/response/header/meta.rs | 13 +- src/client/response/header/mime.rs | 105 ++++++----- src/client/response/header/mime/error.rs | 4 + src/client/response/header/status.rs | 32 ++-- src/client/socket.rs | 75 -------- src/client/socket/connection.rs | 121 ------------- src/client/socket/connection/error.rs | 7 - src/client/socket/connection/input.rs | 158 ----------------- src/client/socket/connection/input/buffer.rs | 87 ---------- src/client/socket/connection/input/error.rs | 5 - src/client/socket/connection/output.rs | 71 -------- src/client/socket/connection/output/error.rs | 3 - src/client/socket/error.rs | 3 - tests/client.rs | 22 +-- 22 files changed, 323 insertions(+), 747 deletions(-) create mode 100644 src/client/buffer.rs rename src/client/{socket/connection/input => }/buffer/error.rs (61%) create mode 100644 src/client/response/header/mime/error.rs delete mode 100644 src/client/socket.rs delete mode 100644 src/client/socket/connection.rs delete mode 100644 src/client/socket/connection/error.rs delete mode 100644 src/client/socket/connection/input.rs delete mode 100644 src/client/socket/connection/input/buffer.rs delete mode 100644 src/client/socket/connection/input/error.rs delete mode 100644 src/client/socket/connection/output.rs delete mode 100644 src/client/socket/connection/output/error.rs delete mode 100644 src/client/socket/error.rs diff --git a/README.md b/README.md index dc1c027..ebc4727 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # ggemini -Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/) +Glib-oriented network library for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! > +This library initially created as extension for [Yoda Browser](https://github.com/YGGverse/Yoda), +but also could be useful for any other integration as depends of +[glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only. + ## Install ``` @@ -14,65 +18,22 @@ cargo add ggemini ## Usage -## `client` +### `client` +[Gio](https://docs.gtk.org/gio/) API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), +so this Client just bit extends some features for Gemini Protocol. -#### `client::single_socket_request_async` - -High-level API to make async socket request, auto-close connection on complete. - -Return [Response](#clientresponseresponse) on success or [Error](#clienterror) enum on failure - -``` rust -use glib::{Uri, UriFlags}; - -// Parse URL string to valid Glib URI object -match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { - // Begin async request - Ok(uri) => ggemini::client::single_socket_request_async(uri, |result| match result { - // Process response - Ok(response) => { - // Expect success status - assert!(match response.header().status() { - Some(ggemini::client::response::header::Status::Success) => true, - _ => false, - }) - } - Err(_) => assert!(false), - }), - Err(_) => assert!(false), -} -``` - -**Pay attention:** - -* Response [Buffer](#clientsocketconnectioninputbufferBuffer) limited to default `capacity` (`0x400`) and `max_size` (`0xfffff`). If you want to change these values, use low-level API to setup connection manually. -* To use [Cancelable](https://docs.gtk.org/gio/class.Cancellable.html) or async Priority values, take a look at [connection](#clientsocketconnection) methods. - -#### `client::Error` +#### `client::buffer` #### `client::response` + +Response parser for [InputStream](https://docs.gtk.org/gio/class.InputStream.html) + #### `client::response::Response` - #### `client::response::header` -#### `client::response::header::meta` -#### `client::response::header::mime` -#### `client::response::header::status` -#### `client::response::header::language` -#### `client::response::header::charset` - #### `client::response::body` -#### `client::socket` -#### `client::socket::connection` -#### `client::socket::connection::input` -#### `client::socket::connection::input::buffer` -#### `client::socket::connection::input::buffer::Buffer` -#### `client::socket::connection::output` - -## Integrations - -* [Yoda](https://github.com/YGGverse/Yoda) - Browser for Gemini Protocol +https://docs.gtk.org/glib/struct.Bytes.html ## See also diff --git a/src/client.rs b/src/client.rs index 8b05a36..2be3ea9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,45 +1,7 @@ +pub mod buffer; pub mod error; pub mod response; -pub mod socket; +pub use buffer::Buffer; pub use error::Error; pub use response::Response; -pub use socket::Socket; - -use glib::Uri; - -/// High-level API to make single async request -/// -/// 1. open new socket connection for [Uri](https://docs.gtk.org/glib/struct.Uri.html) -/// 2. send request -/// 3. read response -/// 4. close connection -/// 5. return `Result` to `callback` function -pub fn single_socket_request_async( - uri: Uri, - callback: impl FnOnce(Result) + 'static, -) { - Socket::new().connect_async(uri.clone(), None, move |connect| match connect { - Ok(connection) => { - connection.request_async(uri, None, None, None, move |connection, response| { - connection.close_async( - None, - None, - Some(|close| { - callback(match close { - Ok(_) => match response { - Ok(buffer) => match Response::from_utf8(&buffer) { - Ok(response) => Ok(response), - Err(_) => Err(Error::Response), - }, - Err(_) => Err(Error::Request), - }, - Err(_) => Err(Error::Close), - }) - }), - ); - }) - } - Err(_) => callback(Err(Error::Connection)), - }); -} diff --git a/src/client/buffer.rs b/src/client/buffer.rs new file mode 100644 index 0000000..f3e8d50 --- /dev/null +++ b/src/client/buffer.rs @@ -0,0 +1,164 @@ +pub mod error; +pub use error::Error; + +use gio::{ + prelude::{IOStreamExt, InputStreamExt}, + Cancellable, SocketConnection, +}; +use glib::{Bytes, Priority}; + +pub const DEFAULT_CAPACITY: usize = 0x400; +pub const DEFAULT_MAX_SIZE: usize = 0xfffff; + +/// Dynamically allocated [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) buffer +/// with configurable `capacity` and `max_size` limits +pub struct Buffer { + buffer: Vec, + max_size: usize, +} + +impl Buffer { + // Constructors + + /// Create new `Self` with default `capacity` and `max_size` preset + pub fn new() -> Self { + Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) + } + + /// Create new `Self` with options + /// + /// Options: + /// * `capacity` initial bytes request to reduce extra memory reallocation (`DEFAULT_CAPACITY` if `None`) + /// * `max_size` max bytes to prevent memory overflow by unknown stream source (`DEFAULT_MAX_SIZE` if `None`) + pub fn new_with_options(capacity: Option, max_size: Option) -> Self { + Self { + buffer: Vec::with_capacity(match capacity { + Some(value) => value, + None => DEFAULT_CAPACITY, + }), + max_size: match max_size { + Some(value) => value, + None => DEFAULT_MAX_SIZE, + }, + } + } + + // Intentable constructors + + /// Simplest way to create `Self` buffer from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + /// + /// Options: + /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from + /// * `callback` function to apply on all async operations complete, return `Result)>` + pub fn from_connection_async( + connection: SocketConnection, + callback: impl FnOnce(Result)>) + 'static, + ) { + Self::read_all_async(Self::new(), connection, None, None, None, callback); + } + + // Actions + + /// Asynchronously read all [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + /// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to `Self.buffer` + /// + /// Useful to grab entire stream without risk of memory overflow (according to `Self.max_size`), + /// reduce extra memory reallocations by `capacity` option. + /// + /// **Notes** + /// + /// We are using entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) reference + /// instead of [InputStream](https://docs.gtk.org/gio/class.InputStream.html) directly just to keep main connection alive in the async context + /// + /// **Options** + /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from + /// * `cancellable` - [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) or `None::<&Cancellable>` by default + /// * `priority` - [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default + /// * `chunk` optional bytes count to read per chunk (`0x100` by default) + /// * `callback` function to apply on all async operations complete, return `Result)>` + pub fn read_all_async( + mut self, + connection: SocketConnection, + cancelable: Option, + priority: Option, + chunk: Option, + callback: impl FnOnce(Result)>) + 'static, + ) { + connection.input_stream().read_bytes_async( + match chunk { + Some(value) => value, + None => 0x100, + }, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + move |result| match result { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return callback(Ok(self)); + } + + // Save chunk to buffer + if let Err(reason) = self.push(bytes) { + return callback(Err((reason, None))); + }; + + // Continue bytes read.. + self.read_all_async(connection, cancelable, priority, chunk, callback); + } + Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), + }, + ); + } + + /// Push [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to `Self.buffer` + /// + /// Return `Error::Overflow` on `max_size` reached + pub fn push(&mut self, bytes: Bytes) -> Result { + // Calculate new size value + let total = self.buffer.len() + bytes.len(); + + // Validate overflow + if total > self.max_size { + return Err(Error::Overflow); + } + + // Success + self.buffer.push(bytes); + + Ok(total) + } + + // Setters + + /// Set new `max_size` value, `DEFAULT_MAX_SIZE` if `None` + pub fn set_max_size(&mut self, value: Option) { + self.max_size = match value { + Some(size) => size, + None => DEFAULT_MAX_SIZE, + } + } + + // Getters + + /// Get reference to bytes collected + pub fn buffer(&self) -> &Vec { + &self.buffer + } + + /// Return copy of bytes as UTF-8 vector + pub fn to_utf8(&self) -> Vec { + self.buffer + .iter() + .flat_map(|byte| byte.iter()) + .cloned() + .collect() + } +} diff --git a/src/client/socket/connection/input/buffer/error.rs b/src/client/buffer/error.rs similarity index 61% rename from src/client/socket/connection/input/buffer/error.rs rename to src/client/buffer/error.rs index 68dd893..82c93e6 100644 --- a/src/client/socket/connection/input/buffer/error.rs +++ b/src/client/buffer/error.rs @@ -1,4 +1,4 @@ pub enum Error { + InputStream, Overflow, - StreamChunkRead, } diff --git a/src/client/response.rs b/src/client/response.rs index ce45f1a..866b4e3 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -6,25 +6,31 @@ pub use body::Body; pub use error::Error; pub use header::Header; +use glib::Bytes; + pub struct Response { header: Header, body: Body, } impl Response { - /// Create new `client::Response` + /// Create new `Self` pub fn new(header: Header, body: Body) -> Self { Self { header, body } } - /// Create new `client::Response` from UTF-8 buffer - pub fn from_utf8(buffer: &[u8]) -> Result { - let header = match Header::from_response(buffer) { + /// Construct from [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + /// + /// Useful for [Gio::InputStream](https://docs.gtk.org/gio/class.InputStream.html): + /// * [read_bytes](https://docs.gtk.org/gio/method.InputStream.read_bytes.html) + /// * [read_bytes_async](https://docs.gtk.org/gio/method.InputStream.read_bytes_async.html) + pub fn from(bytes: &Bytes) -> Result { + let header = match Header::from_response(bytes) { Ok(result) => result, Err(_) => return Err(Error::Header), }; - let body = match Body::from_response(buffer) { + let body = match Body::from_response(bytes) { Ok(result) => result, Err(_) => return Err(Error::Body), }; diff --git a/src/client/response/body.rs b/src/client/response/body.rs index ded304c..80d17b5 100644 --- a/src/client/response/body.rs +++ b/src/client/response/body.rs @@ -1,18 +1,18 @@ pub mod error; pub use error::Error; -use glib::GString; +use glib::{Bytes, GString}; pub struct Body { buffer: Vec, } impl Body { - /// Construct from response buffer - pub fn from_response(response: &[u8] /* @TODO */) -> Result { - let start = Self::start(response)?; + // Constructors + pub fn from_response(bytes: &Bytes) -> Result { + let start = Self::start(bytes)?; - let buffer = match response.get(start..) { + let buffer = match bytes.get(start..) { Some(result) => result, None => return Err(Error::Buffer), }; @@ -23,7 +23,7 @@ impl Body { } // Getters - pub fn buffer(&self) -> &Vec { + pub fn buffer(&self) -> &[u8] { &self.buffer } diff --git a/src/client/response/header.rs b/src/client/response/header.rs index ef3da60..5fc953c 100644 --- a/src/client/response/header.rs +++ b/src/client/response/header.rs @@ -8,8 +8,10 @@ pub use meta::Meta; pub use mime::Mime; pub use status::Status; +use glib::Bytes; + pub struct Header { - status: Option, + status: Status, meta: Option, mime: Option, // @TODO @@ -18,35 +20,41 @@ pub struct Header { } impl Header { - /// Construct from response buffer - /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters - pub fn from_response(response: &[u8] /* @TODO */) -> Result { - let end = Self::end(response)?; + // Constructors + pub fn from_response(bytes: &Bytes) -> Result { + // Get header slice of bytes + let end = Self::end(bytes)?; - let buffer = match response.get(..end) { - Some(result) => result, + let bytes = Bytes::from(match bytes.get(..end) { + Some(buffer) => buffer, None => return Err(Error::Buffer), - }; + }); - let meta = match Meta::from_header(buffer) { - Ok(result) => Some(result), - Err(_) => None, - }; + // Status is required, parse to continue + let status = match Status::from_header(&bytes) { + Ok(status) => Ok(status), + Err(reason) => Err(match reason { + status::Error::Decode => Error::StatusDecode, + status::Error::Undefined => Error::StatusUndefined, + }), + }?; - let mime = mime::from_header(buffer); // optional - // let charset = charset::from_header(buffer); @TODO - // let language = language::from_header(buffer); @TODO - - let status = match status::from_header(buffer) { - Ok(result) => Some(result), - Err(_) => None, - }; - - Ok(Self { status, meta, mime }) + // Done + Ok(Self { + status, + meta: match Meta::from_header(&bytes) { + Ok(meta) => Some(meta), + Err(_) => None, + }, + mime: match Mime::from_header(&bytes) { + Ok(mime) => Some(mime), + Err(_) => None, + }, + }) } // Getters - pub fn status(&self) -> &Option { + pub fn status(&self) -> &Status { &self.status } @@ -59,8 +67,10 @@ impl Header { } // Tools - fn end(buffer: &[u8]) -> Result { - for (offset, &byte) in buffer.iter().enumerate() { + + /// Get last header byte (until \r) + fn end(bytes: &Bytes) -> Result { + for (offset, &byte) in bytes.iter().enumerate() { if byte == b'\r' { return Ok(offset); } diff --git a/src/client/response/header/error.rs b/src/client/response/header/error.rs index b81311b..4eeff64 100644 --- a/src/client/response/header/error.rs +++ b/src/client/response/header/error.rs @@ -1,5 +1,6 @@ pub enum Error { Buffer, Format, - Status, + StatusDecode, + StatusUndefined, } diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index dc90ea7..b2e9380 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -1,15 +1,18 @@ pub mod error; pub use error::Error; -use glib::GString; +use glib::{Bytes, GString}; +/// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). +/// +/// Usefult to grab placeholder text on 10, 11, 31 codes processing pub struct Meta { buffer: Vec, } impl Meta { - pub fn from_header(buffer: &[u8] /* @TODO */) -> Result { - let buffer = match buffer.get(2..) { + pub fn from_header(bytes: &Bytes) -> Result { + let buffer = match bytes.get(3..) { Some(bytes) => bytes.to_vec(), None => return Err(Error::Undefined), }; @@ -23,4 +26,8 @@ impl Meta { Err(_) => Err(Error::Undefined), } } + + pub fn buffer(&self) -> &[u8] { + &self.buffer + } } diff --git a/src/client/response/header/mime.rs b/src/client/response/header/mime.rs index 1f6ea44..8f9e2d9 100644 --- a/src/client/response/header/mime.rs +++ b/src/client/response/header/mime.rs @@ -1,6 +1,10 @@ -use glib::{GString, Uri}; +pub mod error; +pub use error::Error; + +use glib::{Bytes, GString, Uri}; use std::path::Path; +/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters pub enum Mime { TextGemini, TextPlain, @@ -10,53 +14,58 @@ pub enum Mime { ImageWebp, } // @TODO -pub fn from_header(buffer: &[u8] /* @TODO */) -> Option { - from_string(&match GString::from_utf8(buffer.to_vec()) { - Ok(result) => result, - Err(_) => return None, // @TODO error handler? - }) -} +impl Mime { + pub fn from_header(bytes: &Bytes) -> Result { + match bytes.get(..) { + Some(bytes) => match GString::from_utf8(bytes.to_vec()) { + Ok(string) => Self::from_string(string.as_str()), + Err(_) => Err(Error::Decode), + }, + None => Err(Error::Undefined), + } + } -pub fn from_path(path: &Path) -> Option { - match path.extension().and_then(|extension| extension.to_str()) { - Some("gmi") | Some("gemini") => Some(Mime::TextGemini), - Some("txt") => Some(Mime::TextPlain), - Some("png") => Some(Mime::ImagePng), - Some("gif") => Some(Mime::ImageGif), - Some("jpeg") | Some("jpg") => Some(Mime::ImageJpeg), - Some("webp") => Some(Mime::ImageWebp), - _ => None, + pub fn from_path(path: &Path) -> Result { + match path.extension().and_then(|extension| extension.to_str()) { + Some("gmi" | "gemini") => Ok(Self::TextGemini), + Some("txt") => Ok(Self::TextPlain), + Some("png") => Ok(Self::ImagePng), + Some("gif") => Ok(Self::ImageGif), + Some("jpeg" | "jpg") => Ok(Self::ImageJpeg), + Some("webp") => Ok(Self::ImageWebp), + _ => Err(Error::Undefined), + } + } + + pub fn from_string(value: &str) -> Result { + if value.contains("text/gemini") { + return Ok(Self::TextGemini); + } + + if value.contains("text/plain") { + return Ok(Self::TextPlain); + } + + if value.contains("image/gif") { + return Ok(Self::ImageGif); + } + + if value.contains("image/jpeg") { + return Ok(Self::ImageJpeg); + } + + if value.contains("image/webp") { + return Ok(Self::ImageWebp); + } + + if value.contains("image/png") { + return Ok(Self::ImagePng); + } + + Err(Error::Undefined) + } + + pub fn from_uri(uri: &Uri) -> Result { + Self::from_path(Path::new(&uri.to_string())) } } - -pub fn from_string(value: &str) -> Option { - if value.contains("text/gemini") { - return Some(Mime::TextGemini); - } - - if value.contains("text/plain") { - return Some(Mime::TextPlain); - } - - if value.contains("image/gif") { - return Some(Mime::ImageGif); - } - - if value.contains("image/jpeg") { - return Some(Mime::ImageJpeg); - } - - if value.contains("image/webp") { - return Some(Mime::ImageWebp); - } - - if value.contains("image/png") { - return Some(Mime::ImagePng); - } - - None -} - -pub fn from_uri(uri: &Uri) -> Option { - from_path(Path::new(&uri.to_string())) -} diff --git a/src/client/response/header/mime/error.rs b/src/client/response/header/mime/error.rs new file mode 100644 index 0000000..e0c05eb --- /dev/null +++ b/src/client/response/header/mime/error.rs @@ -0,0 +1,4 @@ +pub enum Error { + Decode, + Undefined, +} diff --git a/src/client/response/header/status.rs b/src/client/response/header/status.rs index 841460f..908ddf3 100644 --- a/src/client/response/header/status.rs +++ b/src/client/response/header/status.rs @@ -1,7 +1,7 @@ pub mod error; pub use error::Error; -use glib::GString; +use glib::{Bytes, GString}; /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes pub enum Status { @@ -11,21 +11,23 @@ pub enum Status { Redirect, } // @TODO -pub fn from_header(buffer: &[u8] /* @TODO */) -> Result { - match buffer.get(0..2) { - Some(bytes) => match GString::from_utf8(bytes.to_vec()) { - Ok(string) => from_string(string.as_str()), - Err(_) => Err(Error::Decode), - }, - None => Err(Error::Undefined), +impl Status { + pub fn from_header(bytes: &Bytes) -> Result { + match bytes.get(0..2) { + Some(bytes) => match GString::from_utf8(bytes.to_vec()) { + Ok(string) => Self::from_string(string.as_str()), + Err(_) => Err(Error::Decode), + }, + None => Err(Error::Undefined), + } } -} -pub fn from_string(code: &str) -> Result { - match code { - "10" => Ok(Status::Input), - "11" => Ok(Status::SensitiveInput), - "20" => Ok(Status::Success), - _ => Err(Error::Undefined), + pub fn from_string(code: &str) -> Result { + match code { + "10" => Ok(Self::Input), + "11" => Ok(Self::SensitiveInput), + "20" => Ok(Self::Success), + _ => Err(Error::Undefined), + } } } diff --git a/src/client/socket.rs b/src/client/socket.rs deleted file mode 100644 index 4e541f7..0000000 --- a/src/client/socket.rs +++ /dev/null @@ -1,75 +0,0 @@ -pub mod connection; -pub mod error; - -pub use connection::Connection; -pub use error::Error; - -pub const DEFAULT_PORT: u16 = 1965; - -use gio::{ - prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificateFlags, -}; -use glib::Uri; - -pub struct Socket { - client: SocketClient, - default_port: u16, -} - -impl Socket { - // Constructors - - /// Create new `gio::SocketClient` preset for Gemini Protocol - pub fn new() -> Self { - let client = SocketClient::new(); - - client.set_protocol(SocketProtocol::Tcp); - client.set_tls_validation_flags(TlsCertificateFlags::INSECURE); - client.set_tls(true); - - Self { - client, - default_port: DEFAULT_PORT, - } - } - - // Actions - pub fn connect_async( - &self, - uri: Uri, - cancelable: Option, - callback: impl FnOnce(Result) + 'static, - ) { - self.client.connect_to_uri_async( - uri.to_str().as_str(), - self.default_port, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - |result| { - callback(match result { - Ok(connection) => Ok(Connection::new_from(connection)), - Err(_) => Err(Error::Connection), - }) - }, - ); - } - - // Setters - - /// Change default port for this socket connections (`1965` by default) - pub fn set_default_port(&mut self, default_port: u16) { - self.default_port = default_port; - } - - // Getters - - /// Get reference to `gio::SocketClient` - /// - /// https://docs.gtk.org/gio/class.SocketClient.html - pub fn client(&self) -> &SocketClient { - &self.client - } -} diff --git a/src/client/socket/connection.rs b/src/client/socket/connection.rs deleted file mode 100644 index 255a803..0000000 --- a/src/client/socket/connection.rs +++ /dev/null @@ -1,121 +0,0 @@ -pub mod error; -pub mod input; -pub mod output; - -pub use error::Error; -pub use input::Input; -pub use output::Output; - -use gio::{prelude::IOStreamExt, Cancellable, SocketConnection}; -use glib::{gformat, Bytes, Priority, Uri}; - -pub struct Connection { - connection: SocketConnection, -} - -impl Connection { - // Constructors - - /// Create new `Self` from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - pub fn new_from(connection: SocketConnection) -> Self { - Self { connection } - } - - // Actions - - /// Middle-level API to make async socket request for current connection: - /// - /// 1. send request for [Uri](https://docs.gtk.org/glib/struct.Uri.html) - /// to the ouput [OutputStream](https://docs.gtk.org/gio/class.OutputStream.html); - /// 2. write entire [InputStream](https://docs.gtk.org/gio/class.InputStream.html) - /// into `Vec` buffer on success; - /// 3. return taken `Self` with `Result(Vec, Error)` on complete. - pub fn request_async( - self, - uri: Uri, - cancelable: Option, - priority: Option, - chunk: Option, - callback: impl FnOnce(Self, Result, Error>) + 'static, - ) { - Output::new_from_stream(self.connection.output_stream()).write_async( - &Bytes::from(gformat!("{}\r\n", uri.to_str()).as_bytes()), - cancelable.clone(), - priority, - move |output| match output { - Ok(_) => { - Input::new_from_stream(self.connection.input_stream()).read_all_async( - cancelable.clone(), - priority, - chunk, - move |this, input| { - callback( - self, - match input { - Ok(()) => Ok(this.buffer().to_utf8()), - Err(error) => Err(match error { - input::Error::BufferOverflow => Error::InputBufferOverflow, - input::Error::BufferWrite => Error::InputBufferWrite, - input::Error::StreamChunkRead => { - Error::InputStreamChunkRead - } - }), - }, - ); - }, - ); - } - Err(error) => { - callback( - self, - Err(match error { - output::Error::StreamWrite => Error::OutputStreamWrite, - }), - ); - } - }, - ); - } - - /// Asynchronously close current connection - /// - /// Options: - /// * `cancellable` see [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) (`None::<&Cancellable>` by default) - /// * `priority` [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default - /// * `callback` optional function to apply on complete or `None` to skip - pub fn close_async( - &self, - cancelable: Option, - priority: Option, - callback: Option) + 'static>, - ) { - self.connection.close_async( - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - |result| { - if let Some(call) = callback { - call(match result { - Ok(_) => Ok(()), - Err(_) => Err(Error::Close), - }) - } - }, - ); - } - - // Getters - - /// Get reference to `gio::SocketConnection` - /// - /// https://docs.gtk.org/gio/class.SocketConnection.html - pub fn connection(&self) -> &SocketConnection { - &self.connection - } -} diff --git a/src/client/socket/connection/error.rs b/src/client/socket/connection/error.rs deleted file mode 100644 index 7405d6a..0000000 --- a/src/client/socket/connection/error.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub enum Error { - Close, - InputBufferOverflow, - InputBufferWrite, - InputStreamChunkRead, - OutputStreamWrite, -} diff --git a/src/client/socket/connection/input.rs b/src/client/socket/connection/input.rs deleted file mode 100644 index 1c062ee..0000000 --- a/src/client/socket/connection/input.rs +++ /dev/null @@ -1,158 +0,0 @@ -pub mod buffer; -pub mod error; - -pub use buffer::Buffer; -pub use error::Error; - -use gio::{prelude::InputStreamExt, Cancellable, InputStream}; -use glib::Priority; - -pub const DEFAULT_READ_CHUNK: usize = 0x100; - -pub struct Input { - buffer: Buffer, - stream: InputStream, -} - -impl Input { - // Constructors - - /// Create new `Input` from `gio::InputStream` - /// - /// https://docs.gtk.org/gio/class.InputStream.html - pub fn new_from_stream(stream: InputStream) -> Self { - Self { - buffer: Buffer::new(), - stream, - } - } - - // Actions - - /// Synchronously read all bytes from `gio::InputStream` to `input::Buffer` - /// - /// Return `Self` with `buffer` updated on success - /// - /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html - /// * `chunk` max bytes to read per chunk (256 by default) - pub fn read_all( - mut self, - cancelable: Option, - chunk: Option, - ) -> Result { - loop { - // Continue bytes reading - match self.stream.read_bytes( - match chunk { - Some(value) => value, - None => DEFAULT_READ_CHUNK, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - ) { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return Ok(self); - } - - // Save chunk to buffer - match self.buffer.push(bytes) { - Ok(_) => continue, - Err(buffer::Error::Overflow) => return Err(Error::BufferOverflow), - Err(_) => return Err(Error::BufferWrite), - }; - } - Err(_) => return Err(Error::StreamChunkRead), - }; - } - } - - /// Asynchronously read all bytes from `gio::InputStream` to `input::Buffer` - /// - /// * applies `callback` function on last byte reading complete; - /// * return `Self` with `buffer` updated on success - /// - /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) - /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) - /// * `chunk` optional max bytes to read per chunk (`DEFAULT_READ_CHUNK` by default) - /// * `callback` user function to apply on async iteration complete or `None` to skip - pub fn read_all_async( - mut self, - cancelable: Option, - priority: Option, - chunk: Option, - callback: impl FnOnce(Self, Result<(), Error>) + 'static, - ) { - // Continue bytes reading - self.stream.clone().read_bytes_async( - match chunk { - Some(value) => value, - None => DEFAULT_READ_CHUNK, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - move |result| { - match result { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return callback(self, Ok(())); - } - - // Save chunk to buffer - match self.buffer.push(bytes) { - Err(buffer::Error::Overflow) => { - return callback(self, Err(Error::BufferOverflow)) - } - - // Other errors related to write issues @TODO test - Err(_) => return callback(self, Err(Error::BufferWrite)), - - // Async function, nothing to return yet - _ => (), - }; - - // Continue bytes reading... - self.read_all_async(cancelable, priority, chunk, callback); - } - Err(_) => callback(self, Err(Error::StreamChunkRead)), - } - }, - ); - } - - // Setters - - pub fn set_buffer(&mut self, buffer: Buffer) { - self.buffer = buffer; - } - - pub fn set_stream(&mut self, stream: InputStream) { - self.stream = stream; - } - - // Getters - - /// Get reference to `Buffer` - pub fn buffer(&self) -> &Buffer { - &self.buffer - } - - /// Get reference to `gio::InputStream` - pub fn stream(&self) -> &InputStream { - &self.stream - } -} diff --git a/src/client/socket/connection/input/buffer.rs b/src/client/socket/connection/input/buffer.rs deleted file mode 100644 index b9264c7..0000000 --- a/src/client/socket/connection/input/buffer.rs +++ /dev/null @@ -1,87 +0,0 @@ -pub mod error; -pub use error::Error; - -use glib::Bytes; - -pub const DEFAULT_CAPACITY: usize = 0x400; -pub const DEFAULT_MAX_SIZE: usize = 0xfffff; - -pub struct Buffer { - bytes: Vec, - max_size: usize, -} - -impl Buffer { - // Constructors - - /// Create new dynamically allocated `Buffer` with default `capacity` and `max_size` limit - pub fn new() -> Self { - Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) - } - - /// Create new dynamically allocated `Buffer` with options - /// - /// Options: - /// * `capacity` initial bytes request to reduce extra memory overwrites (1024 by default) - /// * `max_size` max bytes to prevent memory overflow (1M by default) - pub fn new_with_options(capacity: Option, max_size: Option) -> Self { - Self { - bytes: Vec::with_capacity(match capacity { - Some(value) => value, - None => DEFAULT_CAPACITY, - }), - max_size: match max_size { - Some(value) => value, - None => DEFAULT_MAX_SIZE, - }, - } - } - - // Setters - - /// Set new `Buffer.max_size` value to prevent memory overflow - /// - /// Use `DEFAULT_MAX_SIZE` if `None` given. - pub fn set_max_size(&mut self, value: Option) { - self.max_size = match value { - Some(size) => size, - None => DEFAULT_MAX_SIZE, - } - } - - // Actions - - /// Push `glib::Bytes` to `Buffer.bytes` - /// - /// Return `Error::Overflow` on `Buffer.max_size` reached. - pub fn push(&mut self, bytes: Bytes) -> Result { - // Calculate new size value - let total = self.bytes.len() + bytes.len(); - - // Validate overflow - if total > self.max_size { - return Err(Error::Overflow); - } - - // Success - self.bytes.push(bytes); - - Ok(total) - } - - // Getters - - /// Get reference to bytes collected - pub fn bytes(&self) -> &Vec { - &self.bytes - } - - /// Return copy of bytes as UTF-8 vector - pub fn to_utf8(&self) -> Vec { - self.bytes - .iter() - .flat_map(|byte| byte.iter()) - .cloned() - .collect() - } -} diff --git a/src/client/socket/connection/input/error.rs b/src/client/socket/connection/input/error.rs deleted file mode 100644 index 1c383fb..0000000 --- a/src/client/socket/connection/input/error.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub enum Error { - BufferOverflow, - BufferWrite, - StreamChunkRead, -} diff --git a/src/client/socket/connection/output.rs b/src/client/socket/connection/output.rs deleted file mode 100644 index d6f1821..0000000 --- a/src/client/socket/connection/output.rs +++ /dev/null @@ -1,71 +0,0 @@ -pub mod error; - -pub use error::Error; - -use gio::{prelude::OutputStreamExt, Cancellable, OutputStream}; -use glib::{Bytes, Priority}; - -pub struct Output { - stream: OutputStream, -} - -impl Output { - // Constructors - - /// Create new `Output` from `gio::OutputStream` - /// - /// https://docs.gtk.org/gio/class.OutputStream.html - pub fn new_from_stream(stream: OutputStream) -> Self { - Self { stream } - } - - // Actions - - /// Asynchronously write all bytes to `gio::OutputStream`, - /// - /// applies `callback` function on last byte sent. - /// - /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) - /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) - /// * `callback` user function to apply on complete - pub fn write_async( - &self, - bytes: &Bytes, - cancelable: Option, - priority: Option, - callback: impl FnOnce(Result) + 'static, - ) { - self.stream.write_bytes_async( - bytes, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - move |result| { - callback(match result { - Ok(size) => Ok(size), - Err(_) => Err(Error::StreamWrite), - }) - }, - ); - } - - // Setters - - pub fn set_stream(&mut self, stream: OutputStream) { - self.stream = stream; - } - - // Getters - - /// Get reference to `gio::OutputStream` - pub fn stream(&self) -> &OutputStream { - &self.stream - } -} diff --git a/src/client/socket/connection/output/error.rs b/src/client/socket/connection/output/error.rs deleted file mode 100644 index 27547a1..0000000 --- a/src/client/socket/connection/output/error.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub enum Error { - StreamWrite, -} diff --git a/src/client/socket/error.rs b/src/client/socket/error.rs deleted file mode 100644 index 1863964..0000000 --- a/src/client/socket/error.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub enum Error { - Connection, -} diff --git a/tests/client.rs b/tests/client.rs index e98122c..1673a59 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -1,21 +1 @@ -use glib::{Uri, UriFlags}; - -#[test] -fn single_socket_request_async() { - // Parse URI - match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { - // Begin async request - Ok(uri) => ggemini::client::single_socket_request_async(uri, |result| match result { - // Process response - Ok(response) => { - // Expect success status - assert!(match response.header().status() { - Some(ggemini::client::response::header::Status::Success) => true, - _ => false, - }) - } - Err(_) => assert!(false), - }), - Err(_) => assert!(false), - } -} // @TODO async +// @TODO