mirror of
https://github.com/YGGverse/ggemini.git
synced 2026-03-31 17:15:31 +00:00
draft new api version
This commit is contained in:
parent
dccff1e111
commit
9152528790
17 changed files with 299 additions and 317 deletions
20
README.md
20
README.md
|
|
@ -1,14 +1,13 @@
|
||||||
# ggemini
|
# ggemini
|
||||||
|
|
||||||
Glib/Gio-oriented network library for [Gemini protocol](https://geminiprotocol.net/)
|
Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/)
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Project in development!
|
> Project in development!
|
||||||
>
|
>
|
||||||
|
|
||||||
GGemini (or G-Gemini) initially created as the client extension for [Yoda Browser](https://github.com/YGGverse/Yoda),
|
This library mostly written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol,
|
||||||
also could be useful for any other integration as depends of
|
it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only.
|
||||||
[glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`v2_66`) crates only.
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
@ -20,23 +19,16 @@ cargo add ggemini
|
||||||
|
|
||||||
### `client`
|
### `client`
|
||||||
|
|
||||||
Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html),
|
[Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html).
|
||||||
`ggemini::client` just extends some features a bit, to simplify interaction with socket over Gemini Protocol.
|
This library just extend some minimal features wanted for Gemini Protocol
|
||||||
|
|
||||||
It also contain some children components/mods bellow for low-level access any feature directly.
|
|
||||||
|
|
||||||
#### `client::buffer`
|
|
||||||
|
|
||||||
#### `client::response`
|
#### `client::response`
|
||||||
|
|
||||||
Response parser for [InputStream](https://docs.gtk.org/gio/class.InputStream.html)
|
Response parser, currently includes low-level interaction API for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
||||||
|
|
||||||
#### `client::response::Response`
|
|
||||||
#### `client::response::header`
|
#### `client::response::header`
|
||||||
#### `client::response::body`
|
#### `client::response::body`
|
||||||
|
|
||||||
https://docs.gtk.org/glib/struct.Bytes.html
|
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API
|
* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API
|
||||||
|
|
@ -1,7 +1 @@
|
||||||
pub mod buffer;
|
|
||||||
pub mod error;
|
|
||||||
pub mod response;
|
pub mod response;
|
||||||
|
|
||||||
pub use buffer::Buffer;
|
|
||||||
pub use error::Error;
|
|
||||||
pub use response::Response;
|
|
||||||
|
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
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<Bytes>,
|
|
||||||
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<usize>, max_size: Option<usize>) -> 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<Self, (Error, Option<&str>)>`
|
|
||||||
pub fn from_connection_async(
|
|
||||||
connection: SocketConnection,
|
|
||||||
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + '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<Self, (Error, Option<&str>)>`
|
|
||||||
pub fn read_all_async(
|
|
||||||
mut self,
|
|
||||||
connection: SocketConnection,
|
|
||||||
cancelable: Option<Cancellable>,
|
|
||||||
priority: Option<Priority>,
|
|
||||||
chunk: Option<usize>,
|
|
||||||
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + '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::<Cancellable>,
|
|
||||||
}
|
|
||||||
.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<usize, Error> {
|
|
||||||
// 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<usize>) {
|
|
||||||
self.max_size = match value {
|
|
||||||
Some(size) => size,
|
|
||||||
None => DEFAULT_MAX_SIZE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get reference to bytes collected
|
|
||||||
pub fn buffer(&self) -> &Vec<Bytes> {
|
|
||||||
&self.buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return copy of bytes as UTF-8 vector
|
|
||||||
pub fn to_utf8(&self) -> Vec<u8> {
|
|
||||||
self.buffer
|
|
||||||
.iter()
|
|
||||||
.flat_map(|byte| byte.iter())
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
pub enum Error {
|
|
||||||
InputStream,
|
|
||||||
Overflow,
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
pub enum Error {
|
|
||||||
Close,
|
|
||||||
Connection,
|
|
||||||
Request,
|
|
||||||
Response,
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +1,5 @@
|
||||||
pub mod body;
|
pub mod body;
|
||||||
pub mod error;
|
|
||||||
pub mod header;
|
pub mod header;
|
||||||
|
|
||||||
pub use body::Body;
|
pub use body::Body;
|
||||||
pub use error::Error;
|
|
||||||
pub use header::Header;
|
pub use header::Header;
|
||||||
|
|
||||||
use glib::Bytes;
|
|
||||||
|
|
||||||
pub struct Response {
|
|
||||||
header: Header,
|
|
||||||
body: Body,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
/// Create new `Self`
|
|
||||||
pub fn new(header: Header, body: Body) -> Self {
|
|
||||||
Self { header, body }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<Self, Error> {
|
|
||||||
let header = match Header::from_response(bytes) {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(_) => return Err(Error::Header),
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = match Body::from_response(bytes) {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(_) => return Err(Error::Body),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self::new(header, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn header(&self) -> &Header {
|
|
||||||
&self.header
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn body(&self) -> &Body {
|
|
||||||
&self.body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,187 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
use glib::{Bytes, GString};
|
use gio::{
|
||||||
|
prelude::{IOStreamExt, InputStreamExt},
|
||||||
|
Cancellable, SocketConnection,
|
||||||
|
};
|
||||||
|
use glib::{Bytes, GString, Priority};
|
||||||
|
|
||||||
|
pub const DEFAULT_CAPACITY: usize = 0x400;
|
||||||
|
pub const DEFAULT_MAX_SIZE: usize = 0xfffff;
|
||||||
|
|
||||||
|
/// Body container with memory-overflow-safe, dynamically allocated [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) buffer
|
||||||
|
///
|
||||||
|
/// **Features**
|
||||||
|
///
|
||||||
|
/// * configurable `capacity` and `max_size` options
|
||||||
|
/// * build-in [InputStream](https://docs.gtk.org/gio/class.InputStream.html) parser
|
||||||
|
///
|
||||||
|
/// **Notice**
|
||||||
|
///
|
||||||
|
/// * Recommended for gemtext documents
|
||||||
|
/// * For media types, use native stream processors (e.g. [Pixbuf](https://docs.gtk.org/gdk-pixbuf/ctor.Pixbuf.new_from_stream.html) for images)
|
||||||
pub struct Body {
|
pub struct Body {
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<Bytes>,
|
||||||
|
max_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Body {
|
impl Body {
|
||||||
// Constructors
|
// Constructors
|
||||||
pub fn from_response(bytes: &Bytes) -> Result<Self, Error> {
|
|
||||||
let start = Self::start(bytes)?;
|
|
||||||
|
|
||||||
let buffer = match bytes.get(start..) {
|
/// Create new empty `Self` with default `capacity` and `max_size` preset
|
||||||
Some(result) => result,
|
pub fn new() -> Self {
|
||||||
None => return Err(Error::Buffer),
|
Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE))
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(Self {
|
/// Create new new `Self` with options
|
||||||
buffer: Vec::from(buffer),
|
///
|
||||||
})
|
/// 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<usize>, max_size: Option<usize>) -> 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple way to create `Self` buffer from active [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 async operations complete, return `Result<Self, (Error, Option<&str>)>`
|
||||||
|
///
|
||||||
|
/// **Notes**
|
||||||
|
///
|
||||||
|
/// * method requires entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html),
|
||||||
|
/// not just [InputStream](https://docs.gtk.org/gio/class.InputStream.html) because of async features;
|
||||||
|
/// * use this method after `Header` bytes taken from input stream connected (otherwise, take a look on high-level `Response` parser)
|
||||||
|
pub fn from_socket_connection_async(
|
||||||
|
connection: SocketConnection,
|
||||||
|
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + '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` by `chunk`
|
||||||
|
///
|
||||||
|
/// 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) just to keep main connection alive in the async chunks 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<Self, (Error, Option<&str>)>`
|
||||||
|
pub fn read_all_async(
|
||||||
|
mut self,
|
||||||
|
connection: SocketConnection,
|
||||||
|
cancelable: Option<Cancellable>,
|
||||||
|
priority: Option<Priority>,
|
||||||
|
chunk: Option<usize>,
|
||||||
|
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + '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::<Cancellable>,
|
||||||
|
}
|
||||||
|
.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<usize, Error> {
|
||||||
|
// 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<usize>) {
|
||||||
|
self.max_size = match value {
|
||||||
|
Some(size) => size,
|
||||||
|
None => DEFAULT_MAX_SIZE,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
pub fn buffer(&self) -> &[u8] {
|
|
||||||
|
/// Get reference to `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) collected
|
||||||
|
pub fn buffer(&self) -> &Vec<Bytes> {
|
||||||
&self.buffer
|
&self.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return copy of `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) as UTF-8 vector
|
||||||
|
pub fn to_utf8(&self) -> Vec<u8> {
|
||||||
|
self.buffer
|
||||||
|
.iter()
|
||||||
|
.flat_map(|byte| byte.iter())
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intentable getters
|
||||||
|
|
||||||
|
/// Try convert `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to GString
|
||||||
pub fn to_gstring(&self) -> Result<GString, Error> {
|
pub fn to_gstring(&self) -> Result<GString, Error> {
|
||||||
match GString::from_utf8(self.buffer.to_vec()) {
|
match GString::from_utf8(self.to_utf8()) {
|
||||||
Ok(result) => Ok(result),
|
Ok(result) => Ok(result),
|
||||||
Err(_) => Err(Error::Decode),
|
Err(_) => Err(Error::Decode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tools
|
|
||||||
fn start(buffer: &[u8]) -> Result<usize, Error> {
|
|
||||||
for (offset, &byte) in buffer.iter().enumerate() {
|
|
||||||
if byte == b'\n' {
|
|
||||||
return Ok(offset + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(Error::Format)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,6 @@ pub enum Error {
|
||||||
Buffer,
|
Buffer,
|
||||||
Decode,
|
Decode,
|
||||||
Format,
|
Format,
|
||||||
Status,
|
InputStream,
|
||||||
|
Overflow,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
pub enum Error {
|
|
||||||
Header,
|
|
||||||
Body,
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,11 @@ pub use meta::Meta;
|
||||||
pub use mime::Mime;
|
pub use mime::Mime;
|
||||||
pub use status::Status;
|
pub use status::Status;
|
||||||
|
|
||||||
use glib::Bytes;
|
use gio::{
|
||||||
|
prelude::{IOStreamExt, InputStreamExt},
|
||||||
|
Cancellable, SocketConnection,
|
||||||
|
};
|
||||||
|
use glib::{Bytes, Priority};
|
||||||
|
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
status: Status,
|
status: Status,
|
||||||
|
|
@ -21,39 +25,58 @@ pub struct Header {
|
||||||
|
|
||||||
impl Header {
|
impl Header {
|
||||||
// Constructors
|
// Constructors
|
||||||
pub fn from_response(bytes: &Bytes) -> Result<Self, Error> {
|
|
||||||
// Get header slice of bytes
|
|
||||||
let end = Self::end(bytes)?;
|
|
||||||
|
|
||||||
let bytes = Bytes::from(match bytes.get(..end) {
|
pub fn from_socket_connection_async(
|
||||||
Some(buffer) => buffer,
|
socket_connection: SocketConnection,
|
||||||
None => return Err(Error::Buffer),
|
priority: Option<Priority>,
|
||||||
});
|
cancellable: Option<Cancellable>,
|
||||||
|
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
|
||||||
// Status is required, parse to continue
|
) {
|
||||||
let status = match Status::from_header(&bytes) {
|
// Take header buffer from input stream
|
||||||
Ok(status) => Ok(status),
|
Self::read_from_socket_connection_async(
|
||||||
Err(reason) => Err(match reason {
|
Vec::with_capacity(1024),
|
||||||
status::Error::Decode => Error::StatusDecode,
|
socket_connection,
|
||||||
status::Error::Undefined => Error::StatusUndefined,
|
match cancellable {
|
||||||
}),
|
Some(value) => Some(value),
|
||||||
}?;
|
None => None::<Cancellable>,
|
||||||
|
|
||||||
// Done
|
|
||||||
Ok(Self {
|
|
||||||
status,
|
|
||||||
meta: match Meta::from_header(&bytes) {
|
|
||||||
Ok(meta) => Some(meta),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
},
|
||||||
mime: match Mime::from_header(&bytes) {
|
match priority {
|
||||||
Ok(mime) => Some(mime),
|
Some(value) => value,
|
||||||
Err(_) => None,
|
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_header(&buffer) {
|
||||||
|
Ok(meta) => Some(meta),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
mime: match Mime::from_header(&buffer) {
|
||||||
|
Ok(mime) => Some(mime),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Err(reason) => Err((
|
||||||
|
match reason {
|
||||||
|
status::Error::Decode => Error::StatusDecode,
|
||||||
|
status::Error::Undefined => Error::StatusUndefined,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => Err(error),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|
||||||
pub fn status(&self) -> &Status {
|
pub fn status(&self) -> &Status {
|
||||||
&self.status
|
&self.status
|
||||||
}
|
}
|
||||||
|
|
@ -68,13 +91,58 @@ impl Header {
|
||||||
|
|
||||||
// Tools
|
// Tools
|
||||||
|
|
||||||
/// Get last header byte (until \r)
|
pub fn read_from_socket_connection_async(
|
||||||
fn end(bytes: &Bytes) -> Result<usize, Error> {
|
mut buffer: Vec<Bytes>,
|
||||||
for (offset, &byte) in bytes.iter().enumerate() {
|
connection: SocketConnection,
|
||||||
if byte == b'\r' {
|
cancellable: Option<Cancellable>,
|
||||||
return Ok(offset);
|
priority: Priority,
|
||||||
}
|
callback: impl FnOnce(Result<Vec<u8>, (Error, Option<&str>)>) + 'static,
|
||||||
}
|
) {
|
||||||
Err(Error::Format)
|
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() + 1 > 1024 {
|
||||||
|
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())))),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Buffer,
|
Buffer,
|
||||||
Format,
|
InputStream,
|
||||||
|
Protocol,
|
||||||
StatusDecode,
|
StatusDecode,
|
||||||
StatusUndefined,
|
StatusUndefined,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
use glib::{Bytes, GString};
|
use glib::GString;
|
||||||
|
|
||||||
/// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes).
|
/// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes).
|
||||||
///
|
///
|
||||||
|
|
@ -11,13 +11,13 @@ pub struct Meta {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Meta {
|
impl Meta {
|
||||||
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> {
|
pub fn from_header(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
let buffer = match bytes.get(3..) {
|
match buffer.get(3..) {
|
||||||
Some(bytes) => bytes.to_vec(),
|
Some(value) => Ok(Self {
|
||||||
|
buffer: value.to_vec(),
|
||||||
|
}),
|
||||||
None => return Err(Error::Undefined),
|
None => return Err(Error::Undefined),
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(Self { buffer })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_gstring(&self) -> Result<GString, Error> {
|
pub fn to_gstring(&self) -> Result<GString, Error> {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Decode,
|
Decode,
|
||||||
Undefined,
|
Undefined,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
use glib::{Bytes, GString, Uri};
|
use glib::{GString, Uri};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
|
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Mime {
|
pub enum Mime {
|
||||||
TextGemini,
|
TextGemini,
|
||||||
TextPlain,
|
TextPlain,
|
||||||
|
|
@ -15,9 +16,9 @@ pub enum Mime {
|
||||||
} // @TODO
|
} // @TODO
|
||||||
|
|
||||||
impl Mime {
|
impl Mime {
|
||||||
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> {
|
pub fn from_header(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
match bytes.get(..) {
|
match buffer.get(..) {
|
||||||
Some(bytes) => match GString::from_utf8(bytes.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()),
|
||||||
Err(_) => Err(Error::Decode),
|
Err(_) => Err(Error::Decode),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Decode,
|
Decode,
|
||||||
Undefined,
|
Undefined,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
use glib::{Bytes, GString};
|
use glib::GString;
|
||||||
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Input,
|
Input,
|
||||||
SensitiveInput,
|
SensitiveInput,
|
||||||
|
|
@ -12,9 +13,9 @@ pub enum Status {
|
||||||
} // @TODO
|
} // @TODO
|
||||||
|
|
||||||
impl Status {
|
impl Status {
|
||||||
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> {
|
pub fn from_header(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
match bytes.get(0..2) {
|
match buffer.get(0..2) {
|
||||||
Some(bytes) => match GString::from_utf8(bytes.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()),
|
||||||
Err(_) => Err(Error::Decode),
|
Err(_) => Err(Error::Decode),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Decode,
|
Decode,
|
||||||
Undefined,
|
Undefined,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue