diff --git a/Cargo.toml b/Cargo.toml index d5d0ab7..e1ae956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.5.1" +version = "0.6.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index bcc4a0d..e2cf971 100644 --- a/README.md +++ b/README.md @@ -17,21 +17,13 @@ cargo add ggemini ## Usage +* [Documentation](https://docs.rs/ggemini/latest/ggemini/) + _todo_ ### `client` - -[Gio](https://docs.gtk.org/gio/) API already provides powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ -`client` collection just extends some features wanted for Gemini Protocol interaction. - -#### `client::response` -#### `client::response::header` -#### `client::response::body` - ### `gio` -#### `gio::memory_input_stream` - ## See also * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file diff --git a/src/client/response.rs b/src/client/response.rs index a6016ba..cc98b14 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,5 +1,5 @@ pub mod body; -pub mod header; +pub mod meta; pub use body::Body; -pub use header::Header; +pub use meta::Meta; diff --git a/src/client/response/header.rs b/src/client/response/header.rs deleted file mode 100644 index 969696d..0000000 --- a/src/client/response/header.rs +++ /dev/null @@ -1,151 +0,0 @@ -pub mod error; -pub mod meta; -pub mod mime; -pub mod status; - -pub use error::Error; -pub use meta::Meta; -pub use mime::Mime; -pub use status::Status; - -use gio::{ - prelude::{IOStreamExt, InputStreamExt}, - Cancellable, SocketConnection, -}; -use glib::{Bytes, Priority}; - -pub const HEADER_BYTES_LEN: usize = 0x400; // 1024 - -pub struct Header { - status: Status, - meta: Option, - mime: Option, - // @TODO - // charset: Option, - // language: Option, -} - -impl Header { - // Constructors - - pub fn from_socket_connection_async( - socket_connection: SocketConnection, - priority: Option, - cancellable: Option, - callback: impl FnOnce(Result)>) + 'static, - ) { - // Take header buffer from input stream - Self::read_from_socket_connection_async( - Vec::with_capacity(HEADER_BYTES_LEN), - socket_connection, - match cancellable { - Some(value) => Some(value), - None => None::, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - |result| { - callback(match result { - Ok(buffer) => { - // Status is required, parse to continue - match Status::from_header(&buffer) { - Ok(status) => Ok(Self { - status, - meta: match Meta::from(&buffer) { - Ok(meta) => Some(meta), - Err(_) => None, // @TODO handle - }, - mime: match Mime::from_header(&buffer) { - Ok(mime) => Some(mime), - Err(_) => None, // @TODO handle - }, - }), - Err(reason) => Err(( - match reason { - status::Error::Decode => Error::StatusDecode, - status::Error::Undefined => Error::StatusUndefined, - status::Error::Protocol => Error::StatusProtocol, - }, - None, - )), - } - } - Err(error) => Err(error), - }) - }, - ); - } - - // Getters - - pub fn status(&self) -> &Status { - &self.status - } - - pub fn mime(&self) -> &Option { - &self.mime - } - - pub fn meta(&self) -> &Option { - &self.meta - } - - // Tools - - pub fn read_from_socket_connection_async( - mut buffer: Vec, - connection: SocketConnection, - cancellable: Option, - priority: Priority, - callback: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, - ) { - connection.input_stream().read_bytes_async( - 1, // do not change! - priority, - cancellable.clone().as_ref(), - move |result| match result { - Ok(bytes) => { - // Expect valid header length - if bytes.len() == 0 || buffer.len() >= HEADER_BYTES_LEN { - return callback(Err((Error::Protocol, None))); - } - - // Read next byte without buffer record - if bytes.contains(&b'\r') { - return Self::read_from_socket_connection_async( - buffer, - connection, - cancellable, - priority, - callback, - ); - } - - // Complete without buffer record - if bytes.contains(&b'\n') { - return callback(Ok(buffer - .iter() - .flat_map(|byte| byte.iter()) - .cloned() - .collect())); // convert to UTF-8 - } - - // Record - buffer.push(bytes); - - // Continue - Self::read_from_socket_connection_async( - buffer, - connection, - cancellable, - priority, - callback, - ); - } - Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), - }, - ); - } -} diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs new file mode 100644 index 0000000..2143b64 --- /dev/null +++ b/src/client/response/meta.rs @@ -0,0 +1,191 @@ +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, InputStreamExt}, + Cancellable, SocketConnection, +}; +use glib::{Bytes, Priority}; + +pub const MAX_LEN: usize = 0x400; // 1024 + +pub struct Meta { + data: Data, + mime: Mime, + status: Status, + // @TODO + // charset: Charset, + // language: Language, +} + +impl Meta { + // Constructors + + /// Create new `Self` from UTF-8 buffer + pub fn from_utf8(buffer: &[u8]) -> Result)> { + let len = buffer.len(); + + match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { + Some(slice) => { + // Parse data + let data = Data::from_utf8(&slice); + + if let Err(reason) = data { + return Err(( + match reason { + data::Error::Decode => Error::DataDecode, + data::Error::Protocol => Error::DataProtocol, + }, + None, + )); + } + + // MIME + + let mime = Mime::from_utf8(&slice); + + if let Err(reason) = mime { + return Err(( + match reason { + mime::Error::Decode => Error::MimeDecode, + mime::Error::Protocol => Error::MimeProtocol, + mime::Error::Undefined => Error::MimeUndefined, + }, + None, + )); + } + + // Status + + let status = Status::from_utf8(&slice); + + if let Err(reason) = status { + return Err(( + match reason { + status::Error::Decode => Error::StatusDecode, + status::Error::Protocol => Error::StatusProtocol, + status::Error::Undefined => Error::StatusUndefined, + }, + None, + )); + } + + Ok(Self { + data: data.unwrap(), + mime: mime.unwrap(), + status: status.unwrap(), + }) + } + None => Err((Error::Protocol, None)), + } + } + + /// Asynchronously create new `Self` from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) + /// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + pub fn from_socket_connection_async( + socket_connection: SocketConnection, + priority: Option, + cancellable: Option, + on_complete: impl FnOnce(Result)>) + 'static, + ) { + read_from_socket_connection_async( + Vec::with_capacity(MAX_LEN), + socket_connection, + match cancellable { + Some(value) => Some(value), + None => None::, + }, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + |result| match result { + Ok(buffer) => on_complete(Self::from_utf8(&buffer)), + Err(reason) => on_complete(Err(reason)), + }, + ); + } + + // Getters + + pub fn status(&self) -> &Status { + &self.status + } + + pub fn data(&self) -> &Data { + &self.data + } + + pub fn mime(&self) -> &Mime { + &self.mime + } +} + +// Tools + +/// Asynchronously take meta bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// +/// * this function implements low-level helper for `Meta::from_socket_connection_async`, also provides public API for external integrations +/// * requires entire `SocketConnection` instead of `InputStream` to keep connection alive in async context +pub fn read_from_socket_connection_async( + mut buffer: Vec, + connection: SocketConnection, + cancellable: Option, + priority: Priority, + on_complete: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, +) { + connection.input_stream().read_bytes_async( + 1, // do not change! + priority, + cancellable.clone().as_ref(), + move |result| match result { + Ok(bytes) => { + // Expect valid header length + if bytes.len() == 0 || buffer.len() >= MAX_LEN { + return on_complete(Err((Error::Protocol, None))); + } + + // Read next byte without buffer record + if bytes.contains(&b'\r') { + return read_from_socket_connection_async( + buffer, + connection, + cancellable, + priority, + on_complete, + ); + } + + // Complete without buffer record + if bytes.contains(&b'\n') { + return on_complete(Ok(buffer + .iter() + .flat_map(|byte| byte.iter()) + .cloned() + .collect())); // convert to UTF-8 + } + + // Record + buffer.push(bytes); + + // Continue + read_from_socket_connection_async( + buffer, + connection, + cancellable, + priority, + on_complete, + ); + } + Err(reason) => on_complete(Err((Error::InputStream, Some(reason.message())))), + }, + ); +} diff --git a/src/client/response/header/charset.rs b/src/client/response/meta/charset.rs similarity index 100% rename from src/client/response/header/charset.rs rename to src/client/response/meta/charset.rs diff --git a/src/client/response/header/meta.rs b/src/client/response/meta/data.rs similarity index 72% rename from src/client/response/header/meta.rs rename to src/client/response/meta/data.rs index e086d5f..d903f08 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/meta/data.rs @@ -3,25 +3,30 @@ pub use error::Error; use glib::GString; -/// Response meta holder +pub const MAX_LEN: usize = 0x400; // 1024 + +/// Meta data holder for response /// /// Could be created from entire response buffer or just header slice /// /// Use as: /// * placeholder for 10, 11 status /// * URL for 30, 31 status -pub struct Meta { +pub struct Data { value: Option, } -impl Meta { +impl Data { /// Parse Meta from UTF-8 - pub fn from(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { // Init bytes buffer - let mut bytes: Vec = Vec::with_capacity(1021); + let mut bytes: Vec = Vec::with_capacity(MAX_LEN); + + // Calculate len once + let len = buffer.len(); - // Skip 3 bytes for status code of 1024 expected - match buffer.get(3..1021) { + // 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 @@ -37,8 +42,8 @@ impl Meta { match GString::from_utf8(bytes) { Ok(value) => Ok(Self { value: match value.is_empty() { - true => None, false => Some(value), + true => None, }, }), Err(_) => Err(Error::Decode), diff --git a/src/client/response/header/status/error.rs b/src/client/response/meta/data/error.rs similarity index 80% rename from src/client/response/header/status/error.rs rename to src/client/response/meta/data/error.rs index 989e734..125f9c6 100644 --- a/src/client/response/header/status/error.rs +++ b/src/client/response/meta/data/error.rs @@ -2,5 +2,4 @@ pub enum Error { Decode, Protocol, - Undefined, } diff --git a/src/client/response/header/error.rs b/src/client/response/meta/error.rs similarity index 59% rename from src/client/response/header/error.rs rename to src/client/response/meta/error.rs index 17ff132..b1414eb 100644 --- a/src/client/response/header/error.rs +++ b/src/client/response/meta/error.rs @@ -1,7 +1,11 @@ #[derive(Debug)] pub enum Error { - Buffer, + DataDecode, + DataProtocol, InputStream, + MimeDecode, + MimeProtocol, + MimeUndefined, Protocol, StatusDecode, StatusProtocol, diff --git a/src/client/response/header/language.rs b/src/client/response/meta/language.rs similarity index 100% rename from src/client/response/header/language.rs rename to src/client/response/meta/language.rs diff --git a/src/client/response/header/mime.rs b/src/client/response/meta/mime.rs similarity index 89% rename from src/client/response/header/mime.rs rename to src/client/response/meta/mime.rs index d7ea2d0..82e6c14 100644 --- a/src/client/response/header/mime.rs +++ b/src/client/response/meta/mime.rs @@ -4,6 +4,8 @@ pub use error::Error; use glib::{GString, Uri}; use std::path::Path; +pub const MAX_LEN: usize = 0x400; // 1024 + /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters #[derive(Debug)] pub enum Mime { @@ -22,9 +24,10 @@ pub enum Mime { } // @TODO impl Mime { - pub fn from_header(buffer: &[u8]) -> Result { - match buffer.get(..) { - Some(value) => match GString::from_utf8(value.to_vec()) { + pub fn from_utf8(buffer: &[u8]) -> Result { + let len = buffer.len(); + 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(_) => Err(Error::Decode), }, diff --git a/src/client/response/header/meta/error.rs b/src/client/response/meta/mime/error.rs similarity index 100% rename from src/client/response/header/meta/error.rs rename to src/client/response/meta/mime/error.rs diff --git a/src/client/response/header/status.rs b/src/client/response/meta/status.rs similarity index 93% rename from src/client/response/header/status.rs rename to src/client/response/meta/status.rs index e24cbca..2875d31 100644 --- a/src/client/response/header/status.rs +++ b/src/client/response/meta/status.rs @@ -17,7 +17,7 @@ pub enum Status { } // @TODO impl Status { - pub fn from_header(buffer: &[u8]) -> Result { + 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()), diff --git a/src/client/response/header/mime/error.rs b/src/client/response/meta/status/error.rs similarity index 100% rename from src/client/response/header/mime/error.rs rename to src/client/response/meta/status/error.rs