update api version with new implementation

This commit is contained in:
yggverse 2024-10-30 18:28:20 +02:00
parent 47f58f800d
commit 93985095a5
14 changed files with 221 additions and 178 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ggemini" name = "ggemini"
version = "0.5.1" version = "0.6.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"

View file

@ -17,21 +17,13 @@ cargo add ggemini
## Usage ## Usage
* [Documentation](https://docs.rs/ggemini/latest/ggemini/)
_todo_ _todo_
### `client` ### `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`
#### `gio::memory_input_stream`
## See also ## See also
* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API

View file

@ -1,5 +1,5 @@
pub mod body; pub mod body;
pub mod header; pub mod meta;
pub use body::Body; pub use body::Body;
pub use header::Header; pub use meta::Meta;

View file

@ -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<Meta>,
mime: Option<Mime>,
// @TODO
// charset: Option<Charset>,
// language: Option<Language>,
}
impl Header {
// Constructors
pub fn from_socket_connection_async(
socket_connection: SocketConnection,
priority: Option<Priority>,
cancellable: Option<Cancellable>,
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + '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::<Cancellable>,
},
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<Mime> {
&self.mime
}
pub fn meta(&self) -> &Option<Meta> {
&self.meta
}
// Tools
pub fn read_from_socket_connection_async(
mut buffer: Vec<Bytes>,
connection: SocketConnection,
cancellable: Option<Cancellable>,
priority: Priority,
callback: impl FnOnce(Result<Vec<u8>, (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())))),
},
);
}
}

191
src/client/response/meta.rs Normal file
View file

@ -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<Self, (Error, Option<&str>)> {
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<Priority>,
cancellable: Option<Cancellable>,
on_complete: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
) {
read_from_socket_connection_async(
Vec::with_capacity(MAX_LEN),
socket_connection,
match cancellable {
Some(value) => Some(value),
None => None::<Cancellable>,
},
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<Bytes>,
connection: SocketConnection,
cancellable: Option<Cancellable>,
priority: Priority,
on_complete: impl FnOnce(Result<Vec<u8>, (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())))),
},
);
}

View file

@ -3,25 +3,30 @@ pub use error::Error;
use glib::GString; 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 /// Could be created from entire response buffer or just header slice
/// ///
/// Use as: /// Use as:
/// * placeholder for 10, 11 status /// * placeholder for 10, 11 status
/// * URL for 30, 31 status /// * URL for 30, 31 status
pub struct Meta { pub struct Data {
value: Option<GString>, value: Option<GString>,
} }
impl Meta { impl Data {
/// Parse Meta from UTF-8 /// Parse Meta from UTF-8
pub fn from(buffer: &[u8]) -> Result<Self, Error> { pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
// Init bytes buffer // Init bytes buffer
let mut bytes: Vec<u8> = Vec::with_capacity(1021); let mut bytes: Vec<u8> = Vec::with_capacity(MAX_LEN);
// Skip 3 bytes for status code of 1024 expected // Calculate len once
match buffer.get(3..1021) { 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) => { Some(slice) => {
for &byte in slice { for &byte in slice {
// End of header // End of header
@ -37,8 +42,8 @@ impl Meta {
match GString::from_utf8(bytes) { match GString::from_utf8(bytes) {
Ok(value) => Ok(Self { Ok(value) => Ok(Self {
value: match value.is_empty() { value: match value.is_empty() {
true => None,
false => Some(value), false => Some(value),
true => None,
}, },
}), }),
Err(_) => Err(Error::Decode), Err(_) => Err(Error::Decode),

View file

@ -2,5 +2,4 @@
pub enum Error { pub enum Error {
Decode, Decode,
Protocol, Protocol,
Undefined,
} }

View file

@ -1,7 +1,11 @@
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Buffer, DataDecode,
DataProtocol,
InputStream, InputStream,
MimeDecode,
MimeProtocol,
MimeUndefined,
Protocol, Protocol,
StatusDecode, StatusDecode,
StatusProtocol, StatusProtocol,

View file

@ -4,6 +4,8 @@ pub use error::Error;
use glib::{GString, Uri}; use glib::{GString, Uri};
use std::path::Path; use std::path::Path;
pub const MAX_LEN: usize = 0x400; // 1024
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
#[derive(Debug)] #[derive(Debug)]
pub enum Mime { pub enum Mime {
@ -22,9 +24,10 @@ pub enum Mime {
} // @TODO } // @TODO
impl Mime { impl Mime {
pub fn from_header(buffer: &[u8]) -> Result<Self, Error> { pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.get(..) { let len = buffer.len();
Some(value) => match GString::from_utf8(value.to_vec()) { 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()), Ok(string) => Self::from_string(string.as_str()),
Err(_) => Err(Error::Decode), Err(_) => Err(Error::Decode),
}, },

View file

@ -17,7 +17,7 @@ pub enum Status {
} // @TODO } // @TODO
impl Status { impl Status {
pub fn from_header(buffer: &[u8]) -> Result<Self, Error> { pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.get(0..2) { match buffer.get(0..2) {
Some(value) => match GString::from_utf8(value.to_vec()) { Some(value) => match GString::from_utf8(value.to_vec()) {
Ok(string) => Self::from_string(string.as_str()), Ok(string) => Self::from_string(string.as_str()),