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