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
8a5f1e2a57
commit
3cde80b6a8
22 changed files with 323 additions and 747 deletions
65
README.md
65
README.md
|
|
@ -1,11 +1,15 @@
|
||||||
# ggemini
|
# ggemini
|
||||||
|
|
||||||
Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/)
|
Glib-oriented network library for [Gemini protocol](https://geminiprotocol.net/)
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Project in development!
|
> 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
|
## Install
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -14,65 +18,22 @@ cargo add ggemini
|
||||||
|
|
||||||
## Usage
|
## 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`
|
#### `client::buffer`
|
||||||
|
|
||||||
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::response`
|
#### `client::response`
|
||||||
|
|
||||||
|
Response parser for [InputStream](https://docs.gtk.org/gio/class.InputStream.html)
|
||||||
|
|
||||||
#### `client::response::Response`
|
#### `client::response::Response`
|
||||||
|
|
||||||
#### `client::response::header`
|
#### `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::response::body`
|
||||||
|
|
||||||
#### `client::socket`
|
https://docs.gtk.org/glib/struct.Bytes.html
|
||||||
#### `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
|
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,7 @@
|
||||||
|
pub mod buffer;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod socket;
|
|
||||||
|
|
||||||
|
pub use buffer::Buffer;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use response::Response;
|
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<Response, Error>` to `callback` function
|
|
||||||
pub fn single_socket_request_async(
|
|
||||||
uri: Uri,
|
|
||||||
callback: impl FnOnce(Result<Response, Error>) + '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)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
164
src/client/buffer.rs
Normal file
164
src/client/buffer.rs
Normal file
|
|
@ -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<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 +1,4 @@
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
InputStream,
|
||||||
Overflow,
|
Overflow,
|
||||||
StreamChunkRead,
|
|
||||||
}
|
}
|
||||||
|
|
@ -6,25 +6,31 @@ pub use body::Body;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use header::Header;
|
pub use header::Header;
|
||||||
|
|
||||||
|
use glib::Bytes;
|
||||||
|
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
header: Header,
|
header: Header,
|
||||||
body: Body,
|
body: Body,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Response {
|
impl Response {
|
||||||
/// Create new `client::Response`
|
/// Create new `Self`
|
||||||
pub fn new(header: Header, body: Body) -> Self {
|
pub fn new(header: Header, body: Body) -> Self {
|
||||||
Self { header, body }
|
Self { header, body }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create new `client::Response` from UTF-8 buffer
|
/// Construct from [Bytes](https://docs.gtk.org/glib/struct.Bytes.html)
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
///
|
||||||
let header = match Header::from_response(buffer) {
|
/// 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,
|
Ok(result) => result,
|
||||||
Err(_) => return Err(Error::Header),
|
Err(_) => return Err(Error::Header),
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = match Body::from_response(buffer) {
|
let body = match Body::from_response(bytes) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(_) => return Err(Error::Body),
|
Err(_) => return Err(Error::Body),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
use glib::GString;
|
use glib::{Bytes, GString};
|
||||||
|
|
||||||
pub struct Body {
|
pub struct Body {
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Body {
|
impl Body {
|
||||||
/// Construct from response buffer
|
// Constructors
|
||||||
pub fn from_response(response: &[u8] /* @TODO */) -> Result<Self, Error> {
|
pub fn from_response(bytes: &Bytes) -> Result<Self, Error> {
|
||||||
let start = Self::start(response)?;
|
let start = Self::start(bytes)?;
|
||||||
|
|
||||||
let buffer = match response.get(start..) {
|
let buffer = match bytes.get(start..) {
|
||||||
Some(result) => result,
|
Some(result) => result,
|
||||||
None => return Err(Error::Buffer),
|
None => return Err(Error::Buffer),
|
||||||
};
|
};
|
||||||
|
|
@ -23,7 +23,7 @@ impl Body {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
pub fn buffer(&self) -> &Vec<u8> {
|
pub fn buffer(&self) -> &[u8] {
|
||||||
&self.buffer
|
&self.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ pub use meta::Meta;
|
||||||
pub use mime::Mime;
|
pub use mime::Mime;
|
||||||
pub use status::Status;
|
pub use status::Status;
|
||||||
|
|
||||||
|
use glib::Bytes;
|
||||||
|
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
status: Option<Status>,
|
status: Status,
|
||||||
meta: Option<Meta>,
|
meta: Option<Meta>,
|
||||||
mime: Option<Mime>,
|
mime: Option<Mime>,
|
||||||
// @TODO
|
// @TODO
|
||||||
|
|
@ -18,35 +20,41 @@ pub struct Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Header {
|
impl Header {
|
||||||
/// Construct from response buffer
|
// Constructors
|
||||||
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
|
pub fn from_response(bytes: &Bytes) -> Result<Self, Error> {
|
||||||
pub fn from_response(response: &[u8] /* @TODO */) -> Result<Self, Error> {
|
// Get header slice of bytes
|
||||||
let end = Self::end(response)?;
|
let end = Self::end(bytes)?;
|
||||||
|
|
||||||
let buffer = match response.get(..end) {
|
let bytes = Bytes::from(match bytes.get(..end) {
|
||||||
Some(result) => result,
|
Some(buffer) => buffer,
|
||||||
None => return Err(Error::Buffer),
|
None => return Err(Error::Buffer),
|
||||||
};
|
});
|
||||||
|
|
||||||
let meta = match Meta::from_header(buffer) {
|
// Status is required, parse to continue
|
||||||
Ok(result) => Some(result),
|
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,
|
||||||
|
}),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Done
|
||||||
|
Ok(Self {
|
||||||
|
status,
|
||||||
|
meta: match Meta::from_header(&bytes) {
|
||||||
|
Ok(meta) => Some(meta),
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
};
|
},
|
||||||
|
mime: match Mime::from_header(&bytes) {
|
||||||
let mime = mime::from_header(buffer); // optional
|
Ok(mime) => Some(mime),
|
||||||
// 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,
|
Err(_) => None,
|
||||||
};
|
},
|
||||||
|
})
|
||||||
Ok(Self { status, meta, mime })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
pub fn status(&self) -> &Option<Status> {
|
pub fn status(&self) -> &Status {
|
||||||
&self.status
|
&self.status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,8 +67,10 @@ impl Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tools
|
// Tools
|
||||||
fn end(buffer: &[u8]) -> Result<usize, Error> {
|
|
||||||
for (offset, &byte) in buffer.iter().enumerate() {
|
/// Get last header byte (until \r)
|
||||||
|
fn end(bytes: &Bytes) -> Result<usize, Error> {
|
||||||
|
for (offset, &byte) in bytes.iter().enumerate() {
|
||||||
if byte == b'\r' {
|
if byte == b'\r' {
|
||||||
return Ok(offset);
|
return Ok(offset);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Buffer,
|
Buffer,
|
||||||
Format,
|
Format,
|
||||||
Status,
|
StatusDecode,
|
||||||
|
StatusUndefined,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub use error::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 {
|
pub struct Meta {
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Meta {
|
impl Meta {
|
||||||
pub fn from_header(buffer: &[u8] /* @TODO */) -> Result<Self, Error> {
|
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> {
|
||||||
let buffer = match buffer.get(2..) {
|
let buffer = match bytes.get(3..) {
|
||||||
Some(bytes) => bytes.to_vec(),
|
Some(bytes) => bytes.to_vec(),
|
||||||
None => return Err(Error::Undefined),
|
None => return Err(Error::Undefined),
|
||||||
};
|
};
|
||||||
|
|
@ -23,4 +26,8 @@ impl Meta {
|
||||||
Err(_) => Err(Error::Undefined),
|
Err(_) => Err(Error::Undefined),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn buffer(&self) -> &[u8] {
|
||||||
|
&self.buffer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
use glib::{GString, Uri};
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
use glib::{Bytes, GString, Uri};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
|
||||||
pub enum Mime {
|
pub enum Mime {
|
||||||
TextGemini,
|
TextGemini,
|
||||||
TextPlain,
|
TextPlain,
|
||||||
|
|
@ -10,53 +14,58 @@ pub enum Mime {
|
||||||
ImageWebp,
|
ImageWebp,
|
||||||
} // @TODO
|
} // @TODO
|
||||||
|
|
||||||
pub fn from_header(buffer: &[u8] /* @TODO */) -> Option<Mime> {
|
impl Mime {
|
||||||
from_string(&match GString::from_utf8(buffer.to_vec()) {
|
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> {
|
||||||
Ok(result) => result,
|
match bytes.get(..) {
|
||||||
Err(_) => return None, // @TODO error handler?
|
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<Mime> {
|
pub fn from_path(path: &Path) -> Result<Self, Error> {
|
||||||
match path.extension().and_then(|extension| extension.to_str()) {
|
match path.extension().and_then(|extension| extension.to_str()) {
|
||||||
Some("gmi") | Some("gemini") => Some(Mime::TextGemini),
|
Some("gmi" | "gemini") => Ok(Self::TextGemini),
|
||||||
Some("txt") => Some(Mime::TextPlain),
|
Some("txt") => Ok(Self::TextPlain),
|
||||||
Some("png") => Some(Mime::ImagePng),
|
Some("png") => Ok(Self::ImagePng),
|
||||||
Some("gif") => Some(Mime::ImageGif),
|
Some("gif") => Ok(Self::ImageGif),
|
||||||
Some("jpeg") | Some("jpg") => Some(Mime::ImageJpeg),
|
Some("jpeg" | "jpg") => Ok(Self::ImageJpeg),
|
||||||
Some("webp") => Some(Mime::ImageWebp),
|
Some("webp") => Ok(Self::ImageWebp),
|
||||||
_ => None,
|
_ => Err(Error::Undefined),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_string(value: &str) -> Option<Mime> {
|
pub fn from_string(value: &str) -> Result<Self, Error> {
|
||||||
if value.contains("text/gemini") {
|
if value.contains("text/gemini") {
|
||||||
return Some(Mime::TextGemini);
|
return Ok(Self::TextGemini);
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.contains("text/plain") {
|
if value.contains("text/plain") {
|
||||||
return Some(Mime::TextPlain);
|
return Ok(Self::TextPlain);
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.contains("image/gif") {
|
if value.contains("image/gif") {
|
||||||
return Some(Mime::ImageGif);
|
return Ok(Self::ImageGif);
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.contains("image/jpeg") {
|
if value.contains("image/jpeg") {
|
||||||
return Some(Mime::ImageJpeg);
|
return Ok(Self::ImageJpeg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.contains("image/webp") {
|
if value.contains("image/webp") {
|
||||||
return Some(Mime::ImageWebp);
|
return Ok(Self::ImageWebp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.contains("image/png") {
|
if value.contains("image/png") {
|
||||||
return Some(Mime::ImagePng);
|
return Ok(Self::ImagePng);
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
Err(Error::Undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_uri(uri: &Uri) -> Option<Mime> {
|
pub fn from_uri(uri: &Uri) -> Result<Self, Error> {
|
||||||
from_path(Path::new(&uri.to_string()))
|
Self::from_path(Path::new(&uri.to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
src/client/response/header/mime/error.rs
Normal file
4
src/client/response/header/mime/error.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub enum Error {
|
||||||
|
Decode,
|
||||||
|
Undefined,
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
use glib::GString;
|
use glib::{Bytes, GString};
|
||||||
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
|
|
@ -11,21 +11,23 @@ pub enum Status {
|
||||||
Redirect,
|
Redirect,
|
||||||
} // @TODO
|
} // @TODO
|
||||||
|
|
||||||
pub fn from_header(buffer: &[u8] /* @TODO */) -> Result<Status, Error> {
|
impl Status {
|
||||||
match buffer.get(0..2) {
|
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> {
|
||||||
|
match bytes.get(0..2) {
|
||||||
Some(bytes) => match GString::from_utf8(bytes.to_vec()) {
|
Some(bytes) => match GString::from_utf8(bytes.to_vec()) {
|
||||||
Ok(string) => from_string(string.as_str()),
|
Ok(string) => Self::from_string(string.as_str()),
|
||||||
Err(_) => Err(Error::Decode),
|
Err(_) => Err(Error::Decode),
|
||||||
},
|
},
|
||||||
None => Err(Error::Undefined),
|
None => Err(Error::Undefined),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_string(code: &str) -> Result<Status, Error> {
|
pub fn from_string(code: &str) -> Result<Self, Error> {
|
||||||
match code {
|
match code {
|
||||||
"10" => Ok(Status::Input),
|
"10" => Ok(Self::Input),
|
||||||
"11" => Ok(Status::SensitiveInput),
|
"11" => Ok(Self::SensitiveInput),
|
||||||
"20" => Ok(Status::Success),
|
"20" => Ok(Self::Success),
|
||||||
_ => Err(Error::Undefined),
|
_ => Err(Error::Undefined),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<Cancellable>,
|
|
||||||
callback: impl FnOnce(Result<Connection, Error>) + 'static,
|
|
||||||
) {
|
|
||||||
self.client.connect_to_uri_async(
|
|
||||||
uri.to_str().as_str(),
|
|
||||||
self.default_port,
|
|
||||||
match cancelable.clone() {
|
|
||||||
Some(value) => Some(value),
|
|
||||||
None => None::<Cancellable>,
|
|
||||||
}
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<u8>` buffer on success;
|
|
||||||
/// 3. return taken `Self` with `Result(Vec<u8>, Error)` on complete.
|
|
||||||
pub fn request_async(
|
|
||||||
self,
|
|
||||||
uri: Uri,
|
|
||||||
cancelable: Option<Cancellable>,
|
|
||||||
priority: Option<Priority>,
|
|
||||||
chunk: Option<usize>,
|
|
||||||
callback: impl FnOnce(Self, Result<Vec<u8>, 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<Cancellable>,
|
|
||||||
priority: Option<Priority>,
|
|
||||||
callback: Option<impl FnOnce(Result<(), Error>) + 'static>,
|
|
||||||
) {
|
|
||||||
self.connection.close_async(
|
|
||||||
match priority {
|
|
||||||
Some(value) => value,
|
|
||||||
None => Priority::DEFAULT,
|
|
||||||
},
|
|
||||||
match cancelable.clone() {
|
|
||||||
Some(value) => Some(value),
|
|
||||||
None => None::<Cancellable>,
|
|
||||||
}
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
pub enum Error {
|
|
||||||
Close,
|
|
||||||
InputBufferOverflow,
|
|
||||||
InputBufferWrite,
|
|
||||||
InputStreamChunkRead,
|
|
||||||
OutputStreamWrite,
|
|
||||||
}
|
|
||||||
|
|
@ -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<Cancellable>,
|
|
||||||
chunk: Option<usize>,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
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::<Cancellable>,
|
|
||||||
}
|
|
||||||
.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<Cancellable>,
|
|
||||||
priority: Option<Priority>,
|
|
||||||
chunk: Option<usize>,
|
|
||||||
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::<Cancellable>,
|
|
||||||
}
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<Bytes>,
|
|
||||||
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<usize>, max_size: Option<usize>) -> 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<usize>) {
|
|
||||||
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<usize, Error> {
|
|
||||||
// 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<Bytes> {
|
|
||||||
&self.bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return copy of bytes as UTF-8 vector
|
|
||||||
pub fn to_utf8(&self) -> Vec<u8> {
|
|
||||||
self.bytes
|
|
||||||
.iter()
|
|
||||||
.flat_map(|byte| byte.iter())
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
pub enum Error {
|
|
||||||
BufferOverflow,
|
|
||||||
BufferWrite,
|
|
||||||
StreamChunkRead,
|
|
||||||
}
|
|
||||||
|
|
@ -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<Cancellable>,
|
|
||||||
priority: Option<Priority>,
|
|
||||||
callback: impl FnOnce(Result<isize, Error>) + 'static,
|
|
||||||
) {
|
|
||||||
self.stream.write_bytes_async(
|
|
||||||
bytes,
|
|
||||||
match priority {
|
|
||||||
Some(value) => value,
|
|
||||||
None => Priority::DEFAULT,
|
|
||||||
},
|
|
||||||
match cancelable.clone() {
|
|
||||||
Some(value) => Some(value),
|
|
||||||
None => None::<Cancellable>,
|
|
||||||
}
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
pub enum Error {
|
|
||||||
StreamWrite,
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
pub enum Error {
|
|
||||||
Connection,
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1 @@
|
||||||
use glib::{Uri, UriFlags};
|
// @TODO
|
||||||
|
|
||||||
#[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
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue