mirror of
https://github.com/YGGverse/ggemini.git
synced 2026-04-01 17:45:35 +00:00
Compare commits
No commits in common. "main" and "0.3.0" have entirely different histories.
92 changed files with 514 additions and 4117 deletions
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1 +0,0 @@
|
||||||
custom: https://yggverse.github.io/#donate
|
|
||||||
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
|
|
@ -1,31 +0,0 @@
|
||||||
name: Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
RUSTFLAGS: -Dwarnings
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Run rustfmt
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
- name: Update packages index
|
|
||||||
run: sudo apt update
|
|
||||||
- name: Install system packages
|
|
||||||
run: sudo apt install -y libglib2.0-dev
|
|
||||||
- name: Run clippy
|
|
||||||
run: cargo clippy --all-targets
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --verbose
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --verbose
|
|
||||||
15
Cargo.toml
15
Cargo.toml
|
|
@ -1,20 +1,19 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ggemini"
|
name = "ggemini"
|
||||||
version = "0.20.1"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
description = "Glib/Gio-oriented network API for Gemini protocol"
|
description = "Glib-oriented client for Gemini protocol"
|
||||||
keywords = ["gemini", "titan", "glib", "gio", "client"]
|
keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"]
|
||||||
categories = ["development-tools", "network-programming", "parsing"]
|
categories = ["development-tools", "network-programming"]
|
||||||
repository = "https://github.com/YGGverse/ggemini"
|
repository = "https://github.com/YGGverse/ggemini"
|
||||||
|
|
||||||
[dependencies.gio]
|
[dependencies.gio]
|
||||||
package = "gio"
|
package = "gio"
|
||||||
version = "0.21.0"
|
version = "0.20.4"
|
||||||
features = ["v2_70"]
|
|
||||||
|
|
||||||
[dependencies.glib]
|
[dependencies.glib]
|
||||||
package = "glib"
|
package = "glib"
|
||||||
version = "0.21.0"
|
version = "0.20.4"
|
||||||
features = ["v2_66"]
|
features = ["v2_66"]
|
||||||
|
|
|
||||||
72
README.md
72
README.md
|
|
@ -1,77 +1,7 @@
|
||||||
# ggemini
|
# ggemini
|
||||||
|
|
||||||

|
Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/)
|
||||||
[](https://docs.rs/ggemini)
|
|
||||||
[](https://crates.io/crates/ggemini)
|
|
||||||
|
|
||||||
Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/)
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Project in development!
|
> Project in development!
|
||||||
>
|
>
|
||||||
|
|
||||||
GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) / [gio](https://crates.io/crates/gio) (`2.66+`) backend.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Debian</summary>
|
|
||||||
<pre>
|
|
||||||
sudo apt install libglib2.0-dev</pre>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Fedora</summary>
|
|
||||||
<pre>
|
|
||||||
sudo dnf install glib2-devel</pre>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo add ggemini
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
* [Documentation](https://docs.rs/ggemini/latest/ggemini/)
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
``` rust
|
|
||||||
use gio::*;
|
|
||||||
use glib::*;
|
|
||||||
|
|
||||||
use ggemini::client::{
|
|
||||||
connection::{request::{Mode, Request}, Response},
|
|
||||||
Client,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
|
||||||
Client::new().request_async(
|
|
||||||
Request::Gemini { // or `Request::Titan`
|
|
||||||
uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(),
|
|
||||||
mode: Mode::HeaderOnly // handle content separately (based on MIME)
|
|
||||||
},
|
|
||||||
Priority::DEFAULT,
|
|
||||||
Cancellable::new(),
|
|
||||||
None, // optional auth `GTlsCertificate`
|
|
||||||
None, // optional TOFU `GTlsCertificate` array
|
|
||||||
|result| match result {
|
|
||||||
Ok((response, _connection)) => match response {
|
|
||||||
Response::Success(success) => match success.mime().unwrap().as_str() {
|
|
||||||
"text/gemini" => todo!(),
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
Err(_) => todo!(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ExitCode::SUCCESS
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Other crates
|
|
||||||
|
|
||||||
* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API
|
|
||||||
115
src/client.rs
115
src/client.rs
|
|
@ -1,115 +1,10 @@
|
||||||
//! High-level client API to interact with Gemini Socket Server:
|
|
||||||
//! * https://geminiprotocol.net/docs/protocol-specification.gmi
|
|
||||||
|
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod response;
|
||||||
|
pub mod socket;
|
||||||
|
|
||||||
pub use connection::{Connection, Request, Response};
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
pub use response::Response;
|
||||||
|
pub use socket::Socket;
|
||||||
|
|
||||||
use gio::{Cancellable, SocketClient, SocketProtocol, TlsCertificate, prelude::SocketClientExt};
|
// @TODO
|
||||||
use glib::Priority;
|
|
||||||
|
|
||||||
// Defaults
|
|
||||||
|
|
||||||
pub const DEFAULT_TIMEOUT: u32 = 30;
|
|
||||||
pub const DEFAULT_SESSION_RESUMPTION: bool = false;
|
|
||||||
|
|
||||||
/// Main point where connect external crate
|
|
||||||
///
|
|
||||||
/// Provides high-level API for session-safe interaction with
|
|
||||||
/// [Gemini](https://geminiprotocol.net) socket server
|
|
||||||
pub struct Client {
|
|
||||||
is_session_resumption: bool,
|
|
||||||
pub socket: SocketClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Client {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self`
|
|
||||||
pub fn new() -> Self {
|
|
||||||
// Init new socket
|
|
||||||
let socket = SocketClient::new();
|
|
||||||
|
|
||||||
// Setup initial configuration for Gemini Protocol
|
|
||||||
socket.set_protocol(SocketProtocol::Tcp);
|
|
||||||
socket.set_timeout(DEFAULT_TIMEOUT);
|
|
||||||
|
|
||||||
// Done
|
|
||||||
Self {
|
|
||||||
is_session_resumption: DEFAULT_SESSION_RESUMPTION,
|
|
||||||
socket,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
/// Make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html),
|
|
||||||
/// callback with new `Response`on success or `Error` on failure
|
|
||||||
/// * compatible with user (certificate) and guest (certificate-less) connection types
|
|
||||||
pub fn request_async(
|
|
||||||
&self,
|
|
||||||
request: Request,
|
|
||||||
priority: Priority,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
client_certificate: Option<TlsCertificate>,
|
|
||||||
server_certificates: Option<Vec<TlsCertificate>>,
|
|
||||||
callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static,
|
|
||||||
) {
|
|
||||||
// Begin new connection
|
|
||||||
// * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid
|
|
||||||
// [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
|
|
||||||
match request.to_network_address(crate::DEFAULT_PORT) {
|
|
||||||
Ok(network_address) => {
|
|
||||||
self.socket
|
|
||||||
.connect_async(&network_address.clone(), Some(&cancellable.clone()), {
|
|
||||||
let is_session_resumption = self.is_session_resumption;
|
|
||||||
move |result| match result {
|
|
||||||
Ok(socket_connection) => {
|
|
||||||
match Connection::build(
|
|
||||||
socket_connection.clone(),
|
|
||||||
network_address,
|
|
||||||
client_certificate,
|
|
||||||
server_certificates,
|
|
||||||
is_session_resumption,
|
|
||||||
) {
|
|
||||||
Ok(connection) => connection.clone().request_async(
|
|
||||||
request,
|
|
||||||
priority,
|
|
||||||
cancellable,
|
|
||||||
move |result| {
|
|
||||||
callback(match result {
|
|
||||||
Ok(response) => Ok(response),
|
|
||||||
Err(e) => Err(Error::Request(connection, e)),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Err(e) => {
|
|
||||||
callback(Err(Error::Connection(socket_connection, e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => callback(Err(Error::Connect(network_address, e))),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Err(e) => callback(Err(Error::NetworkAddress(e))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setters
|
|
||||||
|
|
||||||
/// Change glib-networking `session-resumption-enabled` property (`false` by default)
|
|
||||||
/// * [Gemini specification](https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates)
|
|
||||||
/// * [GnuTLS manual](https://www.gnutls.org/manual/html_node/Session-resumption.html)
|
|
||||||
pub fn set_session_resumption(&mut self, is_enabled: bool) {
|
|
||||||
self.is_session_resumption = is_enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,172 +1,3 @@
|
||||||
pub mod error;
|
pub mod input_stream;
|
||||||
pub mod request;
|
|
||||||
pub mod response;
|
|
||||||
|
|
||||||
pub use error::Error;
|
// @TODO
|
||||||
pub use request::{Mode, Request};
|
|
||||||
pub use response::Response;
|
|
||||||
|
|
||||||
use gio::{
|
|
||||||
Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection,
|
|
||||||
prelude::{IOStreamExt, OutputStreamExtManual, TlsCertificateExt, TlsConnectionExt},
|
|
||||||
};
|
|
||||||
use glib::{
|
|
||||||
Bytes, Priority,
|
|
||||||
object::{Cast, ObjectExt},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Connection {
|
|
||||||
pub network_address: NetworkAddress,
|
|
||||||
pub socket_connection: SocketConnection,
|
|
||||||
pub tls_client_connection: TlsClientConnection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connection {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self`
|
|
||||||
pub fn build(
|
|
||||||
socket_connection: SocketConnection,
|
|
||||||
network_address: NetworkAddress,
|
|
||||||
client_certificate: Option<TlsCertificate>,
|
|
||||||
server_certificates: Option<Vec<TlsCertificate>>,
|
|
||||||
is_session_resumption: bool,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
Ok(Self {
|
|
||||||
tls_client_connection: match new_tls_client_connection(
|
|
||||||
&socket_connection,
|
|
||||||
Some(&network_address),
|
|
||||||
server_certificates,
|
|
||||||
is_session_resumption,
|
|
||||||
) {
|
|
||||||
Ok(tls_client_connection) => {
|
|
||||||
if let Some(ref c) = client_certificate {
|
|
||||||
tls_client_connection.set_certificate(c);
|
|
||||||
}
|
|
||||||
tls_client_connection
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
},
|
|
||||||
network_address,
|
|
||||||
socket_connection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
/// Send new `Request` to `Self` connection using
|
|
||||||
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or
|
|
||||||
/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol
|
|
||||||
pub fn request_async(
|
|
||||||
self,
|
|
||||||
request: Request,
|
|
||||||
priority: Priority,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
callback: impl FnOnce(Result<(Response, Self), Error>) + 'static,
|
|
||||||
) {
|
|
||||||
let output_stream = self.stream().output_stream();
|
|
||||||
// Make sure **all header bytes** sent to the destination
|
|
||||||
// > A partial write is performed with the size of a message block, which is 16kB
|
|
||||||
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes
|
|
||||||
output_stream.clone().write_all_async(
|
|
||||||
Bytes::from_owned(request.header()),
|
|
||||||
priority,
|
|
||||||
Some(&cancellable.clone()),
|
|
||||||
move |result| match result {
|
|
||||||
Ok(_) => match request {
|
|
||||||
Request::Gemini { mode, .. } => match mode {
|
|
||||||
Mode::HeaderOnly => Response::header_from_connection_async(
|
|
||||||
self,
|
|
||||||
priority,
|
|
||||||
cancellable,
|
|
||||||
|result, connection| {
|
|
||||||
callback(match result {
|
|
||||||
Ok(response) => Ok((response, connection)),
|
|
||||||
Err(e) => Err(Error::Response(e)),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// Make sure **all data bytes** sent to the destination
|
|
||||||
// > A partial write is performed with the size of a message block, which is 16kB
|
|
||||||
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes
|
|
||||||
Request::Titan { data, mode, .. } => output_stream.write_all_async(
|
|
||||||
data,
|
|
||||||
priority,
|
|
||||||
Some(&cancellable.clone()),
|
|
||||||
move |result| match result {
|
|
||||||
Ok(_) => match mode {
|
|
||||||
Mode::HeaderOnly => Response::header_from_connection_async(
|
|
||||||
self,
|
|
||||||
priority,
|
|
||||||
cancellable,
|
|
||||||
|result, connection| {
|
|
||||||
callback(match result {
|
|
||||||
Ok(response) => Ok((response, connection)),
|
|
||||||
Err(e) => Err(Error::Response(e)),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Err((b, e)) => callback(Err(Error::Request(b, e))),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Err((b, e)) => callback(Err(Error::Request(b, e))),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
/// * compatible with user (certificate) and guest (certificate-less) connection type
|
|
||||||
/// * useful to keep `Connection` reference active in async I/O context
|
|
||||||
pub fn stream(&self) -> IOStream {
|
|
||||||
self.tls_client_connection.clone().upcast::<IOStream>()
|
|
||||||
// * also `base_io_stream` method available @TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools
|
|
||||||
|
|
||||||
/// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html)
|
|
||||||
/// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
|
||||||
/// using `server_identity` as the [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
|
|
||||||
fn new_tls_client_connection(
|
|
||||||
socket_connection: &SocketConnection,
|
|
||||||
server_identity: Option<&NetworkAddress>,
|
|
||||||
server_certificates: Option<Vec<TlsCertificate>>,
|
|
||||||
is_session_resumption: bool,
|
|
||||||
) -> Result<TlsClientConnection, Error> {
|
|
||||||
match TlsClientConnection::new(socket_connection, server_identity) {
|
|
||||||
Ok(tls_client_connection) => {
|
|
||||||
// Prevent session resumption (certificate change ability in runtime)
|
|
||||||
tls_client_connection.set_property("session-resumption-enabled", is_session_resumption);
|
|
||||||
|
|
||||||
// Return `Err` on server connection mismatch following specification lines:
|
|
||||||
// > Gemini servers MUST use the TLS close_notify implementation to close the connection
|
|
||||||
// > A client SHOULD notify the user of such a case
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections
|
|
||||||
tls_client_connection.set_require_close_notify(true);
|
|
||||||
|
|
||||||
// [TOFU](https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation)
|
|
||||||
tls_client_connection.connect_accept_certificate(move |_, c, _| {
|
|
||||||
server_certificates
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|server_certificates| {
|
|
||||||
for server_certificate in server_certificates {
|
|
||||||
if server_certificate.is_same(c) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(tls_client_connection)
|
|
||||||
}
|
|
||||||
Err(e) => Err(Error::TlsClientConnection(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Request(glib::Bytes, glib::Error),
|
|
||||||
Response(crate::client::connection::response::Error),
|
|
||||||
TlsClientConnection(glib::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Request(_, e) => {
|
|
||||||
write!(f, "Request error: {e}")
|
|
||||||
}
|
|
||||||
Self::Response(e) => {
|
|
||||||
write!(f, "Response error: {e}")
|
|
||||||
}
|
|
||||||
Self::TlsClientConnection(e) => {
|
|
||||||
write!(f, "TLS client connection error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
src/client/connection/input_stream.rs
Normal file
4
src/client/connection/input_stream.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod byte_buffer;
|
||||||
|
pub use byte_buffer::ByteBuffer;
|
||||||
|
|
||||||
|
// @TODO
|
||||||
158
src/client/connection/input_stream/byte_buffer.rs
Normal file
158
src/client/connection/input_stream/byte_buffer.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
use gio::{prelude::InputStreamExt, Cancellable, InputStream};
|
||||||
|
use glib::{object::IsA, Bytes, Priority};
|
||||||
|
|
||||||
|
pub const DEFAULT_CAPACITY: usize = 0x400;
|
||||||
|
pub const DEFAULT_CHUNK_SIZE: usize = 0x100;
|
||||||
|
pub const DEFAULT_MAX_SIZE: usize = 0xfffff;
|
||||||
|
|
||||||
|
pub struct ByteBuffer {
|
||||||
|
bytes: Vec<Bytes>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ByteBuffer {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new dynamically allocated bytes buffer with default capacity
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_capacity(Some(DEFAULT_CAPACITY))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new dynamically allocated bytes buffer with initial capacity
|
||||||
|
///
|
||||||
|
/// Options:
|
||||||
|
/// * initial bytes request to reduce extra memory overwrites (1024 by default)
|
||||||
|
pub fn with_capacity(value: Option<usize>) -> Self {
|
||||||
|
Self {
|
||||||
|
bytes: Vec::with_capacity(match value {
|
||||||
|
Some(capacity) => capacity,
|
||||||
|
None => DEFAULT_CAPACITY,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readers
|
||||||
|
|
||||||
|
/// Populate bytes buffer synchronously from `gio::InputStream`
|
||||||
|
///
|
||||||
|
/// Options:
|
||||||
|
/// * `input_stream` https://docs.gtk.org/gio/class.InputStream.html
|
||||||
|
/// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html
|
||||||
|
/// * `chunk_size` bytes limit to read per iter (256 by default)
|
||||||
|
/// * `max_size` bytes limit to prevent memory overflow (1M by default)
|
||||||
|
pub fn read_input_stream(
|
||||||
|
mut self,
|
||||||
|
input_stream: InputStream,
|
||||||
|
cancellable: Option<&impl IsA<Cancellable>>,
|
||||||
|
chunk_size: Option<usize>,
|
||||||
|
max_size: Option<usize>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
// Disallow unlimited buffer, use defaults on None
|
||||||
|
let limit = match max_size {
|
||||||
|
Some(value) => value,
|
||||||
|
None => DEFAULT_MAX_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Check buffer size to prevent memory overflow
|
||||||
|
if self.bytes.len() > limit {
|
||||||
|
return Err(Error::Overflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue bytes reading
|
||||||
|
match input_stream.read_bytes(
|
||||||
|
match chunk_size {
|
||||||
|
Some(value) => value,
|
||||||
|
None => DEFAULT_CHUNK_SIZE,
|
||||||
|
},
|
||||||
|
cancellable,
|
||||||
|
) {
|
||||||
|
Ok(bytes) => {
|
||||||
|
// No bytes were read, end of stream
|
||||||
|
if bytes.len() == 0 {
|
||||||
|
return Ok(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save chunk to buffer
|
||||||
|
self.bytes.push(bytes);
|
||||||
|
}
|
||||||
|
Err(_) => return Err(Error::StreamChunkRead),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Populate bytes buffer asynchronously from `gio::InputStream`,
|
||||||
|
/// apply callback function to `Ok(Self)` on success
|
||||||
|
///
|
||||||
|
/// Options:
|
||||||
|
/// * `input_stream` https://docs.gtk.org/gio/class.InputStream.html
|
||||||
|
/// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html
|
||||||
|
/// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html
|
||||||
|
/// * `chunk_size` optional bytes limit to read per iter (256 by default)
|
||||||
|
/// * `max_size` optional bytes limit to prevent memory overflow (1M by default)
|
||||||
|
/// * `callback` user function to apply on complete
|
||||||
|
pub fn read_input_stream_async(
|
||||||
|
mut self,
|
||||||
|
input_stream: InputStream,
|
||||||
|
cancellable: Cancellable,
|
||||||
|
priority: Priority,
|
||||||
|
chunk_size: Option<usize>,
|
||||||
|
max_size: Option<usize>,
|
||||||
|
callback: impl FnOnce(Result<Self, Error>) + 'static,
|
||||||
|
) {
|
||||||
|
// Clone reference counted chunk dependencies
|
||||||
|
let _input_stream = input_stream.clone();
|
||||||
|
let _cancellable = cancellable.clone();
|
||||||
|
|
||||||
|
// Continue bytes reading
|
||||||
|
input_stream.read_bytes_async(
|
||||||
|
match max_size {
|
||||||
|
Some(value) => value,
|
||||||
|
None => DEFAULT_MAX_SIZE,
|
||||||
|
},
|
||||||
|
priority,
|
||||||
|
Some(&cancellable),
|
||||||
|
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
|
||||||
|
self.bytes.push(bytes);
|
||||||
|
|
||||||
|
// Continue bytes reading...
|
||||||
|
self.read_input_stream_async(
|
||||||
|
_input_stream,
|
||||||
|
_cancellable,
|
||||||
|
priority,
|
||||||
|
chunk_size,
|
||||||
|
max_size,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => callback(Err(Error::StreamChunkReadAsync)),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get link to bytes collected
|
||||||
|
pub fn bytes(&self) -> &Vec<Bytes> {
|
||||||
|
&self.bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a copy of the bytes in UTF-8
|
||||||
|
pub fn to_utf8(&self) -> Vec<u8> {
|
||||||
|
self.bytes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|byte| byte.iter())
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/client/connection/input_stream/byte_buffer/error.rs
Normal file
5
src/client/connection/input_stream/byte_buffer/error.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub enum Error {
|
||||||
|
Overflow,
|
||||||
|
StreamChunkRead,
|
||||||
|
StreamChunkReadAsync,
|
||||||
|
}
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub mod mode;
|
|
||||||
|
|
||||||
pub use error::Error;
|
|
||||||
pub use mode::Mode;
|
|
||||||
|
|
||||||
// Local dependencies
|
|
||||||
|
|
||||||
use gio::NetworkAddress;
|
|
||||||
use glib::{Bytes, Uri, UriHideFlags};
|
|
||||||
|
|
||||||
/// Single `Request` implementation for different protocols
|
|
||||||
pub enum Request {
|
|
||||||
Gemini {
|
|
||||||
uri: Uri,
|
|
||||||
mode: Mode,
|
|
||||||
},
|
|
||||||
Titan {
|
|
||||||
uri: Uri,
|
|
||||||
data: Bytes,
|
|
||||||
/// MIME type is optional attribute by Titan protocol specification,
|
|
||||||
/// but server MAY reject the request without `mime` value provided.
|
|
||||||
mime: Option<String>,
|
|
||||||
token: Option<String>,
|
|
||||||
mode: Mode,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Generate header string for `Self`
|
|
||||||
pub fn header(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Gemini { uri, .. } => format!("{uri}\r\n"),
|
|
||||||
Self::Titan {
|
|
||||||
uri,
|
|
||||||
data,
|
|
||||||
mime,
|
|
||||||
token,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let mut header = format!(
|
|
||||||
"{};size={}",
|
|
||||||
uri.to_string_partial(UriHideFlags::QUERY),
|
|
||||||
data.len()
|
|
||||||
);
|
|
||||||
if let Some(mime) = mime {
|
|
||||||
header.push_str(&format!(";mime={mime}"));
|
|
||||||
}
|
|
||||||
if let Some(token) = token {
|
|
||||||
header.push_str(&format!(";token={token}"));
|
|
||||||
}
|
|
||||||
if let Some(query) = uri.query() {
|
|
||||||
header.push_str(&format!("?{query}"));
|
|
||||||
}
|
|
||||||
header.push_str("\r\n");
|
|
||||||
header
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
|
||||||
pub fn uri(&self) -> &Uri {
|
|
||||||
match self {
|
|
||||||
Self::Gemini { uri, .. } => uri,
|
|
||||||
Self::Titan { uri, .. } => uri,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self`
|
|
||||||
pub fn to_network_address(&self, default_port: u16) -> Result<NetworkAddress, Error> {
|
|
||||||
match crate::gio::network_address::from_uri(self.uri(), default_port) {
|
|
||||||
Ok(network_address) => Ok(network_address),
|
|
||||||
Err(e) => Err(Error::NetworkAddress(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_gemini_header() {
|
|
||||||
use glib::UriFlags;
|
|
||||||
|
|
||||||
const REQUEST: &str = "gemini://geminiprotocol.net/";
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Request::Gemini {
|
|
||||||
uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(),
|
|
||||||
mode: Mode::HeaderOnly
|
|
||||||
}
|
|
||||||
.header(),
|
|
||||||
format!("{REQUEST}\r\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_titan_header() {
|
|
||||||
use glib::UriFlags;
|
|
||||||
|
|
||||||
const DATA: &[u8] = &[1, 2, 3];
|
|
||||||
const MIME: &str = "plain/text";
|
|
||||||
const TOKEN: &str = "token";
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Request::Titan {
|
|
||||||
uri: Uri::parse(
|
|
||||||
"titan://geminiprotocol.net/raw/path?key=value",
|
|
||||||
UriFlags::NONE
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
data: Bytes::from(DATA),
|
|
||||||
mime: Some(MIME.to_string()),
|
|
||||||
token: Some(TOKEN.to_string()),
|
|
||||||
mode: Mode::HeaderOnly
|
|
||||||
}
|
|
||||||
.header(),
|
|
||||||
format!(
|
|
||||||
"titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n",
|
|
||||||
DATA.len(),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
NetworkAddress(crate::gio::network_address::error::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::NetworkAddress(e) => {
|
|
||||||
write!(f, "Network Address error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
/// Request modes
|
|
||||||
pub enum Mode {
|
|
||||||
/// Request header bytes only, process content bytes manually
|
|
||||||
/// * useful for manual content type handle: text, stream or large content loaded by chunks
|
|
||||||
HeaderOnly,
|
|
||||||
}
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
pub mod certificate;
|
|
||||||
pub mod error;
|
|
||||||
pub mod failure;
|
|
||||||
pub mod input;
|
|
||||||
pub mod redirect;
|
|
||||||
pub mod success;
|
|
||||||
|
|
||||||
pub use certificate::Certificate;
|
|
||||||
pub use error::{Error, HeaderBytesError};
|
|
||||||
pub use failure::Failure;
|
|
||||||
pub use input::Input;
|
|
||||||
pub use redirect::Redirect;
|
|
||||||
pub use success::Success;
|
|
||||||
|
|
||||||
use super::Connection;
|
|
||||||
use gio::{Cancellable, IOStream};
|
|
||||||
use glib::{Priority, object::IsA};
|
|
||||||
|
|
||||||
const HEADER_LEN: usize = 1024;
|
|
||||||
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#responses
|
|
||||||
pub enum Response {
|
|
||||||
Input(Input), // 1*
|
|
||||||
Success(Success), // 2*
|
|
||||||
Redirect(Redirect), // 3*
|
|
||||||
Failure(Failure), // 4*,5*
|
|
||||||
Certificate(Certificate), // 6*
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
/// Asynchronously create new `Self` for given `Connection`
|
|
||||||
pub fn header_from_connection_async(
|
|
||||||
connection: Connection,
|
|
||||||
priority: Priority,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
callback: impl FnOnce(Result<Self, Error>, Connection) + 'static,
|
|
||||||
) {
|
|
||||||
header_from_stream_async(
|
|
||||||
Vec::with_capacity(HEADER_LEN),
|
|
||||||
connection.stream(),
|
|
||||||
cancellable,
|
|
||||||
priority,
|
|
||||||
|result| {
|
|
||||||
callback(
|
|
||||||
match result {
|
|
||||||
Ok(buffer) => match buffer.first() {
|
|
||||||
Some(b) => match b {
|
|
||||||
b'1' => match Input::from_utf8(&buffer) {
|
|
||||||
Ok(input) => Ok(Self::Input(input)),
|
|
||||||
Err(e) => Err(Error::Input(e)),
|
|
||||||
},
|
|
||||||
b'2' => match Success::from_utf8(&buffer) {
|
|
||||||
Ok(success) => Ok(Self::Success(success)),
|
|
||||||
Err(e) => Err(Error::Success(e)),
|
|
||||||
},
|
|
||||||
b'3' => match Redirect::from_utf8(&buffer) {
|
|
||||||
Ok(redirect) => Ok(Self::Redirect(redirect)),
|
|
||||||
Err(e) => Err(Error::Redirect(e)),
|
|
||||||
},
|
|
||||||
b'4' | b'5' => match Failure::from_utf8(&buffer) {
|
|
||||||
Ok(failure) => Ok(Self::Failure(failure)),
|
|
||||||
Err(e) => Err(Error::Failure(e)),
|
|
||||||
},
|
|
||||||
b'6' => match Certificate::from_utf8(&buffer) {
|
|
||||||
Ok(certificate) => Ok(Self::Certificate(certificate)),
|
|
||||||
Err(e) => Err(Error::Certificate(e)),
|
|
||||||
},
|
|
||||||
b => Err(Error::Code(*b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::Protocol(buffer)),
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
},
|
|
||||||
connection,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools
|
|
||||||
|
|
||||||
/// Asynchronously read header bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
///
|
|
||||||
/// Return UTF-8 buffer collected
|
|
||||||
/// * requires `IOStream` reference to keep `Connection` active in async thread
|
|
||||||
fn header_from_stream_async(
|
|
||||||
mut buffer: Vec<u8>,
|
|
||||||
stream: impl IsA<IOStream>,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
priority: Priority,
|
|
||||||
callback: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
|
|
||||||
) {
|
|
||||||
use gio::prelude::{IOStreamExt, InputStreamExtManual};
|
|
||||||
stream.input_stream().read_async(
|
|
||||||
vec![0],
|
|
||||||
priority,
|
|
||||||
Some(&cancellable.clone()),
|
|
||||||
move |result| match result {
|
|
||||||
Ok((bytes, size)) => {
|
|
||||||
if size == 0 {
|
|
||||||
return callback(Ok(buffer));
|
|
||||||
}
|
|
||||||
if buffer.len() + bytes.len() > HEADER_LEN {
|
|
||||||
buffer.extend(bytes);
|
|
||||||
return callback(Err(Error::Protocol(buffer)));
|
|
||||||
}
|
|
||||||
if bytes[0] == b'\r' {
|
|
||||||
buffer.extend(bytes);
|
|
||||||
return header_from_stream_async(
|
|
||||||
buffer,
|
|
||||||
stream,
|
|
||||||
cancellable,
|
|
||||||
priority,
|
|
||||||
callback,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if bytes[0] == b'\n' {
|
|
||||||
buffer.extend(bytes);
|
|
||||||
return callback(Ok(buffer));
|
|
||||||
}
|
|
||||||
buffer.extend(bytes);
|
|
||||||
header_from_stream_async(buffer, stream, cancellable, priority, callback)
|
|
||||||
}
|
|
||||||
Err((data, e)) => callback(Err(Error::Stream(e, data))),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes slice
|
|
||||||
/// * common for all child parsers
|
|
||||||
fn header_bytes(buffer: &[u8]) -> Result<&[u8], HeaderBytesError> {
|
|
||||||
for (i, b) in buffer.iter().enumerate() {
|
|
||||||
if i > 1024 {
|
|
||||||
return Err(HeaderBytesError::Len);
|
|
||||||
}
|
|
||||||
if *b == b'\r' {
|
|
||||||
let n = i + 1;
|
|
||||||
if buffer.get(n).is_some_and(|b| *b == b'\n') {
|
|
||||||
return Ok(&buffer[..n + 1]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(HeaderBytesError::End)
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub mod not_authorized;
|
|
||||||
pub mod not_valid;
|
|
||||||
pub mod required;
|
|
||||||
|
|
||||||
pub use error::Error;
|
|
||||||
pub use not_authorized::NotAuthorized;
|
|
||||||
pub use not_valid::NotValid;
|
|
||||||
pub use required::Required;
|
|
||||||
|
|
||||||
const CODE: u8 = b'6';
|
|
||||||
|
|
||||||
/// 6* status code group
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates
|
|
||||||
pub enum Certificate {
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
|
|
||||||
Required(Required),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
|
|
||||||
NotAuthorized(NotAuthorized),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
|
|
||||||
NotValid(NotValid),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Certificate {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from buffer include header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
match buffer.first() {
|
|
||||||
Some(b) => match *b {
|
|
||||||
CODE => match buffer.get(1) {
|
|
||||||
Some(b) => match *b {
|
|
||||||
b'0' => Ok(Self::Required(
|
|
||||||
Required::from_utf8(buffer).map_err(Error::Required)?,
|
|
||||||
)),
|
|
||||||
b'1' => Ok(Self::NotAuthorized(
|
|
||||||
NotAuthorized::from_utf8(buffer).map_err(Error::NotAuthorized)?,
|
|
||||||
)),
|
|
||||||
b'2' => Ok(Self::NotValid(
|
|
||||||
NotValid::from_utf8(buffer).map_err(Error::NotValid)?,
|
|
||||||
)),
|
|
||||||
b => Err(Error::SecondByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedSecondByte),
|
|
||||||
},
|
|
||||||
b => Err(Error::FirstByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedFirstByte),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty (not provided by server)
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Self::Required(required) => required.message(),
|
|
||||||
Self::NotAuthorized(not_authorized) => not_authorized.message(),
|
|
||||||
Self::NotValid(not_valid) => not_valid.message(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Required(required) => required.message_or_default(),
|
|
||||||
Self::NotAuthorized(not_authorized) => not_authorized.message_or_default(),
|
|
||||||
Self::NotValid(not_valid) => not_valid.message_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Required(required) => required.as_str(),
|
|
||||||
Self::NotAuthorized(not_authorized) => not_authorized.as_str(),
|
|
||||||
Self::NotValid(not_valid) => not_valid.as_str(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
match self {
|
|
||||||
Self::Required(required) => required.as_bytes(),
|
|
||||||
Self::NotAuthorized(not_authorized) => not_authorized.as_bytes(),
|
|
||||||
Self::NotValid(not_valid) => not_valid.as_bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
fn t(source: &str, message: Option<&str>) {
|
|
||||||
let b = source.as_bytes();
|
|
||||||
let c = Certificate::from_utf8(b).unwrap();
|
|
||||||
assert_eq!(c.message(), message);
|
|
||||||
assert_eq!(c.as_str(), source);
|
|
||||||
assert_eq!(c.as_bytes(), b);
|
|
||||||
}
|
|
||||||
// 60
|
|
||||||
t("60 Required\r\n", Some("Required"));
|
|
||||||
t("60\r\n", None);
|
|
||||||
// 61
|
|
||||||
t("61 Not Authorized\r\n", Some("Not Authorized"));
|
|
||||||
t("61\r\n", None);
|
|
||||||
// 62
|
|
||||||
t("61 Not Valid\r\n", Some("Not Valid"));
|
|
||||||
t("61\r\n", None);
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
FirstByte(u8),
|
|
||||||
NotAuthorized(super::not_authorized::Error),
|
|
||||||
NotValid(super::not_valid::Error),
|
|
||||||
Required(super::required::Error),
|
|
||||||
SecondByte(u8),
|
|
||||||
UndefinedFirstByte,
|
|
||||||
UndefinedSecondByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::FirstByte(b) => {
|
|
||||||
write!(f, "Unexpected first byte: {b}")
|
|
||||||
}
|
|
||||||
Self::NotAuthorized(e) => {
|
|
||||||
write!(f, "NotAuthorized status parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::NotValid(e) => {
|
|
||||||
write!(f, "NotValid status parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::Required(e) => {
|
|
||||||
write!(f, "Required status parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::SecondByte(b) => {
|
|
||||||
write!(f, "Unexpected second byte: {b}")
|
|
||||||
}
|
|
||||||
Self::UndefinedFirstByte => {
|
|
||||||
write!(f, "Undefined first byte")
|
|
||||||
}
|
|
||||||
Self::UndefinedSecondByte => {
|
|
||||||
write!(f, "Undefined second byte")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code
|
|
||||||
pub const CODE: &[u8] = b"61";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Certificate is not authorized";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct NotAuthorized(String);
|
|
||||||
|
|
||||||
impl NotAuthorized {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty (not provided by server)
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let na = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(na.message(), Some("Not Authorized"));
|
|
||||||
assert_eq!(na.message_or_default(), "Not Authorized");
|
|
||||||
assert_eq!(na.as_str(), "61 Not Authorized\r\n");
|
|
||||||
assert_eq!(na.as_bytes(), "61 Not Authorized\r\n".as_bytes());
|
|
||||||
|
|
||||||
let na = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(na.message(), None);
|
|
||||||
assert_eq!(na.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(na.as_str(), "61\r\n");
|
|
||||||
assert_eq!(na.as_bytes(), "61\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(NotAuthorized::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(NotAuthorized::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code
|
|
||||||
pub const CODE: &[u8] = b"62";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Certificate is not valid";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct NotValid(String);
|
|
||||||
|
|
||||||
impl NotValid {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty (not provided by server)
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let nv = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(nv.message(), Some("Not Valid"));
|
|
||||||
assert_eq!(nv.message_or_default(), "Not Valid");
|
|
||||||
assert_eq!(nv.as_str(), "62 Not Valid\r\n");
|
|
||||||
assert_eq!(nv.as_bytes(), "62 Not Valid\r\n".as_bytes());
|
|
||||||
|
|
||||||
let nv = NotValid::from_utf8("62\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(nv.message(), None);
|
|
||||||
assert_eq!(nv.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(nv.as_str(), "62\r\n");
|
|
||||||
assert_eq!(nv.as_bytes(), "62\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
// @TODO assert!(NotValid::from_utf8("62Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(NotValid::from_utf8("63 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(NotValid::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(NotValid::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code
|
|
||||||
pub const CODE: &[u8] = b"60";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Certificate required";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct Required(String);
|
|
||||||
|
|
||||||
impl Required {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty (not provided by server)
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let r = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(r.message(), Some("Required"));
|
|
||||||
assert_eq!(r.message_or_default(), "Required");
|
|
||||||
assert_eq!(r.as_str(), "60 Required\r\n");
|
|
||||||
assert_eq!(r.as_bytes(), "60 Required\r\n".as_bytes());
|
|
||||||
|
|
||||||
let r = Required::from_utf8("60\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(r.message(), None);
|
|
||||||
assert_eq!(r.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(r.as_str(), "60\r\n");
|
|
||||||
assert_eq!(r.as_bytes(), "60\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Required::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Required::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
use std::{
|
|
||||||
fmt::{Display, Formatter, Result},
|
|
||||||
str::Utf8Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Certificate(super::certificate::Error),
|
|
||||||
Code(u8),
|
|
||||||
Failure(super::failure::Error),
|
|
||||||
Input(super::input::Error),
|
|
||||||
Protocol(Vec<u8>),
|
|
||||||
Redirect(super::redirect::Error),
|
|
||||||
Stream(glib::Error, Vec<u8>),
|
|
||||||
Success(super::success::Error),
|
|
||||||
Utf8Error(Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Certificate(e) => {
|
|
||||||
write!(f, "Certificate error: {e}")
|
|
||||||
}
|
|
||||||
Self::Code(b) => {
|
|
||||||
write!(f, "Unexpected status code byte: {b}")
|
|
||||||
}
|
|
||||||
Self::Failure(e) => {
|
|
||||||
write!(f, "Failure error: {e}")
|
|
||||||
}
|
|
||||||
Self::Input(e) => {
|
|
||||||
write!(f, "Input error: {e}")
|
|
||||||
}
|
|
||||||
Self::Protocol(..) => {
|
|
||||||
write!(f, "Protocol error")
|
|
||||||
}
|
|
||||||
Self::Redirect(e) => {
|
|
||||||
write!(f, "Redirect error: {e}")
|
|
||||||
}
|
|
||||||
Self::Stream(e, ..) => {
|
|
||||||
write!(f, "I/O stream error: {e}")
|
|
||||||
}
|
|
||||||
Self::Success(e) => {
|
|
||||||
write!(f, "Success error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum HeaderBytesError {
|
|
||||||
Len,
|
|
||||||
End,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for HeaderBytesError {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Len => {
|
|
||||||
write!(f, "Unexpected header length")
|
|
||||||
}
|
|
||||||
Self::End => {
|
|
||||||
write!(f, "Unexpected header end")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub mod permanent;
|
|
||||||
pub mod temporary;
|
|
||||||
|
|
||||||
pub use error::Error;
|
|
||||||
pub use permanent::Permanent;
|
|
||||||
pub use temporary::Temporary;
|
|
||||||
|
|
||||||
pub enum Failure {
|
|
||||||
/// 4* status code group
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure
|
|
||||||
Temporary(Temporary),
|
|
||||||
/// 5* status code group
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure
|
|
||||||
Permanent(Permanent),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Failure {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from buffer include header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
match buffer.first() {
|
|
||||||
Some(b) => match b {
|
|
||||||
b'4' => match Temporary::from_utf8(buffer) {
|
|
||||||
Ok(input) => Ok(Self::Temporary(input)),
|
|
||||||
Err(e) => Err(Error::Temporary(e)),
|
|
||||||
},
|
|
||||||
b'5' => match Permanent::from_utf8(buffer) {
|
|
||||||
Ok(failure) => Ok(Self::Permanent(failure)),
|
|
||||||
Err(e) => Err(Error::Permanent(e)),
|
|
||||||
},
|
|
||||||
b => Err(Error::Code(*b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::Protocol),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Self::Permanent(permanent) => permanent.message(),
|
|
||||||
Self::Temporary(temporary) => temporary.message(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Permanent(permanent) => permanent.message_or_default(),
|
|
||||||
Self::Temporary(temporary) => temporary.message_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Permanent(permanent) => permanent.as_str(),
|
|
||||||
Self::Temporary(temporary) => temporary.as_str(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
match self {
|
|
||||||
Self::Permanent(permanent) => permanent.as_bytes(),
|
|
||||||
Self::Temporary(temporary) => temporary.as_bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
fn t(source: String, message: Option<&str>) {
|
|
||||||
let b = source.as_bytes();
|
|
||||||
let i = Failure::from_utf8(b).unwrap();
|
|
||||||
assert_eq!(i.message(), message);
|
|
||||||
assert_eq!(i.as_str(), source);
|
|
||||||
assert_eq!(i.as_bytes(), b);
|
|
||||||
}
|
|
||||||
for code in [40, 41, 42, 43, 44, 50, 51, 52, 53, 59] {
|
|
||||||
t(format!("{code} Message\r\n"), Some("Message"));
|
|
||||||
t(format!("{code}\r\n"), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code(u8),
|
|
||||||
Permanent(super::permanent::Error),
|
|
||||||
Protocol,
|
|
||||||
Temporary(super::temporary::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code(b) => {
|
|
||||||
write!(f, "Unexpected status code byte: {b}")
|
|
||||||
}
|
|
||||||
Self::Permanent(e) => {
|
|
||||||
write!(f, "Permanent failure group error: {e}")
|
|
||||||
}
|
|
||||||
Self::Protocol => {
|
|
||||||
write!(f, "Protocol error")
|
|
||||||
}
|
|
||||||
Self::Temporary(e) => {
|
|
||||||
write!(f, "Temporary failure group error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
pub mod bad_request;
|
|
||||||
pub mod default;
|
|
||||||
pub mod error;
|
|
||||||
pub mod gone;
|
|
||||||
pub mod not_found;
|
|
||||||
pub mod proxy_request_refused;
|
|
||||||
|
|
||||||
pub use bad_request::BadRequest;
|
|
||||||
pub use default::Default;
|
|
||||||
pub use error::Error;
|
|
||||||
pub use gone::Gone;
|
|
||||||
pub use not_found::NotFound;
|
|
||||||
pub use proxy_request_refused::ProxyRequestRefused;
|
|
||||||
|
|
||||||
const CODE: u8 = b'5';
|
|
||||||
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure
|
|
||||||
pub enum Permanent {
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50
|
|
||||||
Default(Default),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found
|
|
||||||
NotFound(NotFound),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone
|
|
||||||
Gone(Gone),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused
|
|
||||||
ProxyRequestRefused(ProxyRequestRefused),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request
|
|
||||||
BadRequest(BadRequest),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Permanent {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from buffer include header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
match buffer.first() {
|
|
||||||
Some(b) => match *b {
|
|
||||||
CODE => match buffer.get(1) {
|
|
||||||
Some(b) => match *b {
|
|
||||||
b'0' => Ok(Self::Default(
|
|
||||||
Default::from_utf8(buffer).map_err(Error::Default)?,
|
|
||||||
)),
|
|
||||||
b'1' => Ok(Self::NotFound(
|
|
||||||
NotFound::from_utf8(buffer).map_err(Error::NotFound)?,
|
|
||||||
)),
|
|
||||||
b'2' => Ok(Self::Gone(Gone::from_utf8(buffer).map_err(Error::Gone)?)),
|
|
||||||
b'3' => Ok(Self::ProxyRequestRefused(
|
|
||||||
ProxyRequestRefused::from_utf8(buffer)
|
|
||||||
.map_err(Error::ProxyRequestRefused)?,
|
|
||||||
)),
|
|
||||||
b'9' => Ok(Self::BadRequest(
|
|
||||||
BadRequest::from_utf8(buffer).map_err(Error::BadRequest)?,
|
|
||||||
)),
|
|
||||||
b => Err(Error::SecondByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedSecondByte),
|
|
||||||
},
|
|
||||||
b => Err(Error::FirstByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedFirstByte),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.message(),
|
|
||||||
Self::NotFound(not_found) => not_found.message(),
|
|
||||||
Self::Gone(gone) => gone.message(),
|
|
||||||
Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.message(),
|
|
||||||
Self::BadRequest(bad_request) => bad_request.message(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.message_or_default(),
|
|
||||||
Self::NotFound(not_found) => not_found.message_or_default(),
|
|
||||||
Self::Gone(gone) => gone.message_or_default(),
|
|
||||||
Self::ProxyRequestRefused(proxy_request_refused) => {
|
|
||||||
proxy_request_refused.message_or_default()
|
|
||||||
}
|
|
||||||
Self::BadRequest(bad_request) => bad_request.message_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.as_str(),
|
|
||||||
Self::NotFound(not_found) => not_found.as_str(),
|
|
||||||
Self::Gone(gone) => gone.as_str(),
|
|
||||||
Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_str(),
|
|
||||||
Self::BadRequest(bad_request) => bad_request.as_str(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.as_bytes(),
|
|
||||||
Self::NotFound(not_found) => not_found.as_bytes(),
|
|
||||||
Self::Gone(gone) => gone.as_bytes(),
|
|
||||||
Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_bytes(),
|
|
||||||
Self::BadRequest(bad_request) => bad_request.as_bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
fn t(source: String, message: Option<&str>) {
|
|
||||||
let b = source.as_bytes();
|
|
||||||
let i = Permanent::from_utf8(b).unwrap();
|
|
||||||
assert_eq!(i.message(), message);
|
|
||||||
assert_eq!(i.as_str(), source);
|
|
||||||
assert_eq!(i.as_bytes(), b);
|
|
||||||
}
|
|
||||||
for code in [50, 51, 52, 53, 59] {
|
|
||||||
t(format!("{code} Message\r\n"), Some("Message"));
|
|
||||||
t(format!("{code}\r\n"), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code
|
|
||||||
pub const CODE: &[u8] = b"59";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Bad request";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct BadRequest(String);
|
|
||||||
|
|
||||||
impl BadRequest {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let br = BadRequest::from_utf8("59 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(br.message(), Some("Message"));
|
|
||||||
assert_eq!(br.message_or_default(), "Message");
|
|
||||||
assert_eq!(br.as_str(), "59 Message\r\n");
|
|
||||||
assert_eq!(br.as_bytes(), "59 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let br = BadRequest::from_utf8("59\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(br.message(), None);
|
|
||||||
assert_eq!(br.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(br.as_str(), "59\r\n");
|
|
||||||
assert_eq!(br.as_bytes(), "59\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(BadRequest::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(BadRequest::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(BadRequest::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code
|
|
||||||
pub const CODE: &[u8] = b"50";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Permanent error";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct Default(String);
|
|
||||||
|
|
||||||
impl Default {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let d = Default::from_utf8("50 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(d.message(), Some("Message"));
|
|
||||||
assert_eq!(d.message_or_default(), "Message");
|
|
||||||
assert_eq!(d.as_str(), "50 Message\r\n");
|
|
||||||
assert_eq!(d.as_bytes(), "50 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let d = Default::from_utf8("50\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(d.message(), None);
|
|
||||||
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(d.as_str(), "50\r\n");
|
|
||||||
assert_eq!(d.as_bytes(), "50\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
BadRequest(super::bad_request::Error),
|
|
||||||
Default(super::default::Error),
|
|
||||||
FirstByte(u8),
|
|
||||||
Gone(super::gone::Error),
|
|
||||||
NotFound(super::not_found::Error),
|
|
||||||
ProxyRequestRefused(super::proxy_request_refused::Error),
|
|
||||||
SecondByte(u8),
|
|
||||||
UndefinedFirstByte,
|
|
||||||
UndefinedSecondByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::BadRequest(e) => {
|
|
||||||
write!(f, "BadRequest parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::Default(e) => {
|
|
||||||
write!(f, "Default parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::FirstByte(b) => {
|
|
||||||
write!(f, "Unexpected first byte: {b}")
|
|
||||||
}
|
|
||||||
Self::Gone(e) => {
|
|
||||||
write!(f, "Gone parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::NotFound(e) => {
|
|
||||||
write!(f, "NotFound parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::ProxyRequestRefused(e) => {
|
|
||||||
write!(f, "ProxyRequestRefused parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::SecondByte(b) => {
|
|
||||||
write!(f, "Unexpected second byte: {b}")
|
|
||||||
}
|
|
||||||
Self::UndefinedFirstByte => {
|
|
||||||
write!(f, "Undefined first byte")
|
|
||||||
}
|
|
||||||
Self::UndefinedSecondByte => {
|
|
||||||
write!(f, "Undefined second byte")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code
|
|
||||||
pub const CODE: &[u8] = b"52";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Resource gone";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct Gone(String);
|
|
||||||
|
|
||||||
impl Gone {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let g = Gone::from_utf8("52 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(g.message(), Some("Message"));
|
|
||||||
assert_eq!(g.message_or_default(), "Message");
|
|
||||||
assert_eq!(g.as_str(), "52 Message\r\n");
|
|
||||||
assert_eq!(g.as_bytes(), "52 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let g = Gone::from_utf8("52\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(g.message(), None);
|
|
||||||
assert_eq!(g.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(g.as_str(), "52\r\n");
|
|
||||||
assert_eq!(g.as_bytes(), "52\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(Gone::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Gone::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Gone::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code
|
|
||||||
pub const CODE: &[u8] = b"51";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Not Found";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct NotFound(String);
|
|
||||||
|
|
||||||
impl NotFound {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let nf = NotFound::from_utf8("51 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(nf.message(), Some("Message"));
|
|
||||||
assert_eq!(nf.message_or_default(), "Message");
|
|
||||||
assert_eq!(nf.as_str(), "51 Message\r\n");
|
|
||||||
assert_eq!(nf.as_bytes(), "51 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let nf = NotFound::from_utf8("51\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(nf.message(), None);
|
|
||||||
assert_eq!(nf.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(nf.as_str(), "51\r\n");
|
|
||||||
assert_eq!(nf.as_bytes(), "51\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(NotFound::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(NotFound::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(NotFound::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code
|
|
||||||
pub const CODE: &[u8] = b"53";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Proxy request refused";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct ProxyRequestRefused(String);
|
|
||||||
|
|
||||||
impl ProxyRequestRefused {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let prf = ProxyRequestRefused::from_utf8("53 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(prf.message(), Some("Message"));
|
|
||||||
assert_eq!(prf.message_or_default(), "Message");
|
|
||||||
assert_eq!(prf.as_str(), "53 Message\r\n");
|
|
||||||
assert_eq!(prf.as_bytes(), "53 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let prf = ProxyRequestRefused::from_utf8("53\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(prf.message(), None);
|
|
||||||
assert_eq!(prf.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(prf.as_str(), "53\r\n");
|
|
||||||
assert_eq!(prf.as_bytes(), "53\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(ProxyRequestRefused::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(ProxyRequestRefused::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(ProxyRequestRefused::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
pub mod cgi_error;
|
|
||||||
pub mod default;
|
|
||||||
pub mod error;
|
|
||||||
pub mod proxy_error;
|
|
||||||
pub mod server_unavailable;
|
|
||||||
pub mod slow_down;
|
|
||||||
|
|
||||||
pub use cgi_error::CgiError;
|
|
||||||
pub use default::Default;
|
|
||||||
pub use error::Error;
|
|
||||||
pub use proxy_error::ProxyError;
|
|
||||||
pub use server_unavailable::ServerUnavailable;
|
|
||||||
pub use slow_down::SlowDown;
|
|
||||||
|
|
||||||
const CODE: u8 = b'4';
|
|
||||||
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure
|
|
||||||
pub enum Temporary {
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40
|
|
||||||
Default(Default),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable
|
|
||||||
ServerUnavailable(ServerUnavailable),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error
|
|
||||||
CgiError(CgiError),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error
|
|
||||||
ProxyError(ProxyError),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down
|
|
||||||
SlowDown(SlowDown),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Temporary {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from buffer include header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
match buffer.first() {
|
|
||||||
Some(b) => match *b {
|
|
||||||
CODE => match buffer.get(1) {
|
|
||||||
Some(b) => match *b {
|
|
||||||
b'0' => Ok(Self::Default(
|
|
||||||
Default::from_utf8(buffer).map_err(Error::Default)?,
|
|
||||||
)),
|
|
||||||
b'1' => Ok(Self::ServerUnavailable(
|
|
||||||
ServerUnavailable::from_utf8(buffer)
|
|
||||||
.map_err(Error::ServerUnavailable)?,
|
|
||||||
)),
|
|
||||||
b'2' => Ok(Self::CgiError(
|
|
||||||
CgiError::from_utf8(buffer).map_err(Error::CgiError)?,
|
|
||||||
)),
|
|
||||||
b'3' => Ok(Self::ProxyError(
|
|
||||||
ProxyError::from_utf8(buffer).map_err(Error::ProxyError)?,
|
|
||||||
)),
|
|
||||||
b'4' => Ok(Self::SlowDown(
|
|
||||||
SlowDown::from_utf8(buffer).map_err(Error::SlowDown)?,
|
|
||||||
)),
|
|
||||||
b => Err(Error::SecondByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedSecondByte),
|
|
||||||
},
|
|
||||||
b => Err(Error::FirstByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedFirstByte),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.message(),
|
|
||||||
Self::ServerUnavailable(server_unavailable) => server_unavailable.message(),
|
|
||||||
Self::CgiError(cgi_error) => cgi_error.message(),
|
|
||||||
Self::ProxyError(proxy_error) => proxy_error.message(),
|
|
||||||
Self::SlowDown(slow_down) => slow_down.message(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.message_or_default(),
|
|
||||||
Self::ServerUnavailable(server_unavailable) => server_unavailable.message_or_default(),
|
|
||||||
Self::CgiError(cgi_error) => cgi_error.message_or_default(),
|
|
||||||
Self::ProxyError(proxy_error) => proxy_error.message_or_default(),
|
|
||||||
Self::SlowDown(slow_down) => slow_down.message_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.as_str(),
|
|
||||||
Self::ServerUnavailable(server_unavailable) => server_unavailable.as_str(),
|
|
||||||
Self::CgiError(cgi_error) => cgi_error.as_str(),
|
|
||||||
Self::ProxyError(proxy_error) => proxy_error.as_str(),
|
|
||||||
Self::SlowDown(slow_down) => slow_down.as_str(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.as_bytes(),
|
|
||||||
Self::ServerUnavailable(server_unavailable) => server_unavailable.as_bytes(),
|
|
||||||
Self::CgiError(cgi_error) => cgi_error.as_bytes(),
|
|
||||||
Self::ProxyError(proxy_error) => proxy_error.as_bytes(),
|
|
||||||
Self::SlowDown(slow_down) => slow_down.as_bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
fn t(source: String, message: Option<&str>) {
|
|
||||||
let b = source.as_bytes();
|
|
||||||
let i = Temporary::from_utf8(b).unwrap();
|
|
||||||
assert_eq!(i.message(), message);
|
|
||||||
assert_eq!(i.as_str(), source);
|
|
||||||
assert_eq!(i.as_bytes(), b);
|
|
||||||
}
|
|
||||||
for code in [40, 41, 42, 43, 44] {
|
|
||||||
t(format!("{code} Message\r\n"), Some("Message"));
|
|
||||||
t(format!("{code}\r\n"), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code
|
|
||||||
pub const CODE: &[u8] = b"42";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "CGI Error";
|
|
||||||
|
|
||||||
/// Hold header `String` for [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct CgiError(String);
|
|
||||||
|
|
||||||
impl CgiError {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let ce = CgiError::from_utf8("42 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(ce.message(), Some("Message"));
|
|
||||||
assert_eq!(ce.message_or_default(), "Message");
|
|
||||||
assert_eq!(ce.as_str(), "42 Message\r\n");
|
|
||||||
assert_eq!(ce.as_bytes(), "42 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let ce = CgiError::from_utf8("42\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(ce.message(), None);
|
|
||||||
assert_eq!(ce.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(ce.as_str(), "42\r\n");
|
|
||||||
assert_eq!(ce.as_bytes(), "42\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(CgiError::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(CgiError::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(CgiError::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code
|
|
||||||
pub const CODE: &[u8] = b"40";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Temporary error";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct Default(String);
|
|
||||||
|
|
||||||
impl Default {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let d = Default::from_utf8("40 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(d.message(), Some("Message"));
|
|
||||||
assert_eq!(d.message_or_default(), "Message");
|
|
||||||
assert_eq!(d.as_str(), "40 Message\r\n");
|
|
||||||
assert_eq!(d.as_bytes(), "40 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let d = Default::from_utf8("40\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(d.message(), None);
|
|
||||||
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(d.as_str(), "40\r\n");
|
|
||||||
assert_eq!(d.as_bytes(), "40\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
CgiError(super::cgi_error::Error),
|
|
||||||
Default(super::default::Error),
|
|
||||||
FirstByte(u8),
|
|
||||||
ProxyError(super::proxy_error::Error),
|
|
||||||
SecondByte(u8),
|
|
||||||
ServerUnavailable(super::server_unavailable::Error),
|
|
||||||
SlowDown(super::slow_down::Error),
|
|
||||||
UndefinedFirstByte,
|
|
||||||
UndefinedSecondByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::CgiError(e) => {
|
|
||||||
write!(f, "CgiError parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::Default(e) => {
|
|
||||||
write!(f, "Default parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::FirstByte(b) => {
|
|
||||||
write!(f, "Unexpected first byte: {b}")
|
|
||||||
}
|
|
||||||
Self::ProxyError(e) => {
|
|
||||||
write!(f, "ProxyError parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::SecondByte(b) => {
|
|
||||||
write!(f, "Unexpected second byte: {b}")
|
|
||||||
}
|
|
||||||
Self::ServerUnavailable(e) => {
|
|
||||||
write!(f, "ServerUnavailable parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::SlowDown(e) => {
|
|
||||||
write!(f, "SlowDown parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::UndefinedFirstByte => {
|
|
||||||
write!(f, "Undefined first byte")
|
|
||||||
}
|
|
||||||
Self::UndefinedSecondByte => {
|
|
||||||
write!(f, "Undefined second byte")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code
|
|
||||||
pub const CODE: &[u8] = b"43";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Proxy error";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct ProxyError(String);
|
|
||||||
|
|
||||||
impl ProxyError {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let pe = ProxyError::from_utf8("43 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(pe.message(), Some("Message"));
|
|
||||||
assert_eq!(pe.message_or_default(), "Message");
|
|
||||||
assert_eq!(pe.as_str(), "43 Message\r\n");
|
|
||||||
assert_eq!(pe.as_bytes(), "43 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let pe = ProxyError::from_utf8("43\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(pe.message(), None);
|
|
||||||
assert_eq!(pe.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(pe.as_str(), "43\r\n");
|
|
||||||
assert_eq!(pe.as_bytes(), "43\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(ProxyError::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(ProxyError::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(ProxyError::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable)
|
|
||||||
/// temporary error status code
|
|
||||||
pub const CODE: &[u8] = b"41";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Server unavailable";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable)
|
|
||||||
/// temporary error status code
|
|
||||||
///
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct ServerUnavailable(String);
|
|
||||||
|
|
||||||
impl ServerUnavailable {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let su = ServerUnavailable::from_utf8("41 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(su.message(), Some("Message"));
|
|
||||||
assert_eq!(su.message_or_default(), "Message");
|
|
||||||
assert_eq!(su.as_str(), "41 Message\r\n");
|
|
||||||
assert_eq!(su.as_bytes(), "41 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let su = ServerUnavailable::from_utf8("41\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(su.message(), None);
|
|
||||||
assert_eq!(su.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(su.as_str(), "41\r\n");
|
|
||||||
assert_eq!(su.as_bytes(), "41\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(ServerUnavailable::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(ServerUnavailable::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(ServerUnavailable::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Slow Down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down)
|
|
||||||
/// temporary error status code
|
|
||||||
pub const CODE: &[u8] = b"44";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Slow down";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down)
|
|
||||||
/// temporary error status code
|
|
||||||
///
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct SlowDown(String);
|
|
||||||
|
|
||||||
impl SlowDown {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let sd = SlowDown::from_utf8("44 Message\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(sd.message(), Some("Message"));
|
|
||||||
assert_eq!(sd.message_or_default(), "Message");
|
|
||||||
assert_eq!(sd.as_str(), "44 Message\r\n");
|
|
||||||
assert_eq!(sd.as_bytes(), "44 Message\r\n".as_bytes());
|
|
||||||
|
|
||||||
let sd = SlowDown::from_utf8("44\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(sd.message(), None);
|
|
||||||
assert_eq!(sd.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(sd.as_str(), "44\r\n");
|
|
||||||
assert_eq!(sd.as_bytes(), "44\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(SlowDown::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(SlowDown::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(SlowDown::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
pub mod default;
|
|
||||||
pub mod error;
|
|
||||||
pub mod sensitive;
|
|
||||||
|
|
||||||
pub use default::Default;
|
|
||||||
pub use error::Error;
|
|
||||||
pub use sensitive::Sensitive;
|
|
||||||
|
|
||||||
const CODE: u8 = b'1';
|
|
||||||
|
|
||||||
/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected)
|
|
||||||
pub enum Input {
|
|
||||||
Default(Default),
|
|
||||||
Sensitive(Sensitive),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Input {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from buffer include header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
match buffer.first() {
|
|
||||||
Some(b) => match *b {
|
|
||||||
CODE => match buffer.get(1) {
|
|
||||||
Some(b) => match *b {
|
|
||||||
b'0' => Ok(Self::Default(
|
|
||||||
Default::from_utf8(buffer).map_err(Error::Default)?,
|
|
||||||
)),
|
|
||||||
b'1' => Ok(Self::Sensitive(
|
|
||||||
Sensitive::from_utf8(buffer).map_err(Error::Sensitive)?,
|
|
||||||
)),
|
|
||||||
b => Err(Error::SecondByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedSecondByte),
|
|
||||||
},
|
|
||||||
b => Err(Error::FirstByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedFirstByte),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.message(),
|
|
||||||
Self::Sensitive(sensitive) => sensitive.message(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.message_or_default(),
|
|
||||||
Self::Sensitive(sensitive) => sensitive.message_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.as_str(),
|
|
||||||
Self::Sensitive(sensitive) => sensitive.as_str(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.as_bytes(),
|
|
||||||
Self::Sensitive(sensitive) => sensitive.as_bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
fn t(source: &str, message: Option<&str>) {
|
|
||||||
let b = source.as_bytes();
|
|
||||||
let i = Input::from_utf8(b).unwrap();
|
|
||||||
assert_eq!(i.message(), message);
|
|
||||||
assert_eq!(i.as_str(), source);
|
|
||||||
assert_eq!(i.as_bytes(), b);
|
|
||||||
}
|
|
||||||
// 10
|
|
||||||
t("10 Default\r\n", Some("Default"));
|
|
||||||
t("10\r\n", None);
|
|
||||||
// 11
|
|
||||||
t("11 Sensitive\r\n", Some("Sensitive"));
|
|
||||||
t("11\r\n", None);
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code
|
|
||||||
pub const CODE: &[u8] = b"10";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Input expected";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct Default(String);
|
|
||||||
|
|
||||||
impl Default {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let d = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(d.message(), Some("Default"));
|
|
||||||
assert_eq!(d.message_or_default(), "Default");
|
|
||||||
assert_eq!(d.as_str(), "10 Default\r\n");
|
|
||||||
assert_eq!(d.as_bytes(), "10 Default\r\n".as_bytes());
|
|
||||||
|
|
||||||
let d = Default::from_utf8("10\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(d.message(), None);
|
|
||||||
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(d.as_str(), "10\r\n");
|
|
||||||
assert_eq!(d.as_bytes(), "10\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Default(super::default::Error),
|
|
||||||
FirstByte(u8),
|
|
||||||
SecondByte(u8),
|
|
||||||
Sensitive(super::sensitive::Error),
|
|
||||||
UndefinedFirstByte,
|
|
||||||
UndefinedSecondByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Default(e) => {
|
|
||||||
write!(f, "Default parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::FirstByte(b) => {
|
|
||||||
write!(f, "Unexpected first byte: {b}")
|
|
||||||
}
|
|
||||||
Self::SecondByte(b) => {
|
|
||||||
write!(f, "Unexpected second byte: {b}")
|
|
||||||
}
|
|
||||||
Self::Sensitive(e) => {
|
|
||||||
write!(f, "Sensitive parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::UndefinedFirstByte => {
|
|
||||||
write!(f, "Undefined first byte")
|
|
||||||
}
|
|
||||||
Self::UndefinedSecondByte => {
|
|
||||||
write!(f, "Undefined second byte")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code
|
|
||||||
pub const CODE: &[u8] = b"11";
|
|
||||||
|
|
||||||
/// Default message if the optional value was not provided by the server
|
|
||||||
/// * useful to skip match cases in external applications,
|
|
||||||
/// by using `super::message_or_default` method.
|
|
||||||
pub const DEFAULT_MESSAGE: &str = "Sensitive input expected";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct Sensitive(String);
|
|
||||||
|
|
||||||
impl Sensitive {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * return `None` if the message is empty
|
|
||||||
pub fn message(&self) -> Option<&str> {
|
|
||||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optional message for `Self`
|
|
||||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
|
||||||
pub fn message_or_default(&self) -> &str {
|
|
||||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
// ok
|
|
||||||
let s = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(s.message(), Some("Sensitive"));
|
|
||||||
assert_eq!(s.message_or_default(), "Sensitive");
|
|
||||||
assert_eq!(s.as_str(), "11 Sensitive\r\n");
|
|
||||||
assert_eq!(s.as_bytes(), "11 Sensitive\r\n".as_bytes());
|
|
||||||
|
|
||||||
let s = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(s.message(), None);
|
|
||||||
assert_eq!(s.message_or_default(), DEFAULT_MESSAGE);
|
|
||||||
assert_eq!(s.as_str(), "11\r\n");
|
|
||||||
assert_eq!(s.as_bytes(), "11\r\n".as_bytes());
|
|
||||||
|
|
||||||
// err
|
|
||||||
assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err());
|
|
||||||
assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub mod permanent;
|
|
||||||
pub mod temporary;
|
|
||||||
|
|
||||||
pub use error::{Error, UriError};
|
|
||||||
pub use permanent::Permanent;
|
|
||||||
pub use temporary::Temporary;
|
|
||||||
|
|
||||||
// Local dependencies
|
|
||||||
|
|
||||||
use glib::{Uri, UriFlags};
|
|
||||||
|
|
||||||
const CODE: u8 = b'3';
|
|
||||||
|
|
||||||
/// [Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) statuses
|
|
||||||
pub enum Redirect {
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
|
|
||||||
Temporary(Temporary),
|
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
|
|
||||||
Permanent(Permanent),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Redirect {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from buffer include header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
match buffer.first() {
|
|
||||||
Some(b) => match *b {
|
|
||||||
CODE => match buffer.get(1) {
|
|
||||||
Some(b) => match *b {
|
|
||||||
b'0' => Ok(Self::Temporary(
|
|
||||||
Temporary::from_utf8(buffer).map_err(Error::Temporary)?,
|
|
||||||
)),
|
|
||||||
b'1' => Ok(Self::Permanent(
|
|
||||||
Permanent::from_utf8(buffer).map_err(Error::Permanent)?,
|
|
||||||
)),
|
|
||||||
b => Err(Error::SecondByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedSecondByte),
|
|
||||||
},
|
|
||||||
b => Err(Error::FirstByte(b)),
|
|
||||||
},
|
|
||||||
None => Err(Error::UndefinedFirstByte),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
pub fn target(&self) -> Result<&str, Error> {
|
|
||||||
match self {
|
|
||||||
Self::Temporary(temporary) => temporary.target().map_err(Error::Temporary),
|
|
||||||
Self::Permanent(permanent) => permanent.target().map_err(Error::Permanent),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Temporary(temporary) => temporary.as_str(),
|
|
||||||
Self::Permanent(permanent) => permanent.as_str(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
match self {
|
|
||||||
Self::Temporary(temporary) => temporary.as_bytes(),
|
|
||||||
Self::Permanent(permanent) => permanent.as_bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uri(&self, base: &Uri) -> Result<Uri, Error> {
|
|
||||||
match self {
|
|
||||||
Self::Temporary(temporary) => temporary.uri(base).map_err(Error::Temporary),
|
|
||||||
Self::Permanent(permanent) => permanent.uri(base).map_err(Error::Permanent),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools
|
|
||||||
|
|
||||||
/// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection),
|
|
||||||
/// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base`
|
|
||||||
/// * fragment implementation uncompleted @TODO
|
|
||||||
fn uri(target: &str, base: &Uri) -> Result<Uri, UriError> {
|
|
||||||
match Uri::build(
|
|
||||||
UriFlags::NONE,
|
|
||||||
base.scheme().as_str(),
|
|
||||||
None, // unexpected
|
|
||||||
base.host().as_deref(),
|
|
||||||
base.port(),
|
|
||||||
base.path().as_str(),
|
|
||||||
// > If a server sends a redirection in response to a request with a query string,
|
|
||||||
// > the client MUST NOT apply the query string to the new location
|
|
||||||
None,
|
|
||||||
// > A server SHOULD NOT include fragments in redirections,
|
|
||||||
// > but if one is given, and a client already has a fragment it could apply (from the original URI),
|
|
||||||
// > it is up to the client which fragment to apply.
|
|
||||||
None, // @TODO
|
|
||||||
)
|
|
||||||
.parse_relative(
|
|
||||||
&{
|
|
||||||
// URI started with double slash yet not supported by Glib function
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
|
|
||||||
let t = target;
|
|
||||||
match t.strip_prefix("//") {
|
|
||||||
Some(p) => {
|
|
||||||
let postfix = p.trim_start_matches(":");
|
|
||||||
format!(
|
|
||||||
"{}://{}",
|
|
||||||
base.scheme(),
|
|
||||||
if postfix.is_empty() {
|
|
||||||
match base.host() {
|
|
||||||
Some(h) => format!("{h}/"),
|
|
||||||
None => return Err(UriError::BaseHost),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
postfix.to_string()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => t.to_string(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UriFlags::NONE,
|
|
||||||
) {
|
|
||||||
Ok(absolute) => Ok(absolute),
|
|
||||||
Err(e) => Err(UriError::ParseRelative(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
/// Test common assertion rules
|
|
||||||
fn t(base: &Uri, source: &str, target: &str) {
|
|
||||||
let b = source.as_bytes();
|
|
||||||
let r = Redirect::from_utf8(b).unwrap();
|
|
||||||
assert!(r.uri(base).is_ok_and(|u| u.to_string() == target));
|
|
||||||
assert_eq!(r.as_str(), source);
|
|
||||||
assert_eq!(r.as_bytes(), b);
|
|
||||||
}
|
|
||||||
// common base
|
|
||||||
let base = Uri::build(
|
|
||||||
UriFlags::NONE,
|
|
||||||
"gemini",
|
|
||||||
None,
|
|
||||||
Some("geminiprotocol.net"),
|
|
||||||
-1,
|
|
||||||
"/path/",
|
|
||||||
Some("query"),
|
|
||||||
Some("fragment"),
|
|
||||||
);
|
|
||||||
// codes test
|
|
||||||
t(
|
|
||||||
&base,
|
|
||||||
"30 gemini://geminiprotocol.net/path\r\n",
|
|
||||||
"gemini://geminiprotocol.net/path",
|
|
||||||
);
|
|
||||||
t(
|
|
||||||
&base,
|
|
||||||
"31 gemini://geminiprotocol.net/path\r\n",
|
|
||||||
"gemini://geminiprotocol.net/path",
|
|
||||||
);
|
|
||||||
// relative test
|
|
||||||
t(
|
|
||||||
&base,
|
|
||||||
"31 path\r\n",
|
|
||||||
"gemini://geminiprotocol.net/path/path",
|
|
||||||
);
|
|
||||||
t(
|
|
||||||
&base,
|
|
||||||
"31 //geminiprotocol.net\r\n",
|
|
||||||
"gemini://geminiprotocol.net",
|
|
||||||
);
|
|
||||||
t(
|
|
||||||
&base,
|
|
||||||
"31 //geminiprotocol.net/path\r\n",
|
|
||||||
"gemini://geminiprotocol.net/path",
|
|
||||||
);
|
|
||||||
t(&base, "31 /path\r\n", "gemini://geminiprotocol.net/path");
|
|
||||||
t(&base, "31 //:\r\n", "gemini://geminiprotocol.net/");
|
|
||||||
t(&base, "31 //\r\n", "gemini://geminiprotocol.net/");
|
|
||||||
t(&base, "31 /\r\n", "gemini://geminiprotocol.net/");
|
|
||||||
t(&base, "31 ../\r\n", "gemini://geminiprotocol.net/");
|
|
||||||
t(&base, "31 ..\r\n", "gemini://geminiprotocol.net/");
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
FirstByte(u8),
|
|
||||||
Permanent(super::permanent::Error),
|
|
||||||
SecondByte(u8),
|
|
||||||
Temporary(super::temporary::Error),
|
|
||||||
UndefinedFirstByte,
|
|
||||||
UndefinedSecondByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::FirstByte(b) => {
|
|
||||||
write!(f, "Unexpected first byte: {b}")
|
|
||||||
}
|
|
||||||
Self::Permanent(e) => {
|
|
||||||
write!(f, "Permanent parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::SecondByte(b) => {
|
|
||||||
write!(f, "Unexpected second byte: {b}")
|
|
||||||
}
|
|
||||||
Self::Temporary(e) => {
|
|
||||||
write!(f, "Temporary parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::UndefinedFirstByte => {
|
|
||||||
write!(f, "Undefined first byte")
|
|
||||||
}
|
|
||||||
Self::UndefinedSecondByte => {
|
|
||||||
write!(f, "Undefined second byte")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle `super::uri` method
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum UriError {
|
|
||||||
BaseHost,
|
|
||||||
ParseRelative(glib::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for UriError {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::BaseHost => {
|
|
||||||
write!(f, "URI base host required")
|
|
||||||
}
|
|
||||||
Self::ParseRelative(e) => {
|
|
||||||
write!(f, "URI parse relative error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
// Local dependencies
|
|
||||||
|
|
||||||
use glib::Uri;
|
|
||||||
|
|
||||||
/// [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code
|
|
||||||
pub const CODE: &[u8] = b"31";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct Permanent(String);
|
|
||||||
|
|
||||||
impl Permanent {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get raw target for `Self`
|
|
||||||
/// * return `Err` if the required target is empty
|
|
||||||
pub fn target(&self) -> Result<&str, Error> {
|
|
||||||
self.0
|
|
||||||
.get(2..)
|
|
||||||
.map(|s| s.trim())
|
|
||||||
.filter(|x| !x.is_empty())
|
|
||||||
.ok_or(Error::TargetEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uri(&self, base: &Uri) -> Result<Uri, Error> {
|
|
||||||
super::uri(self.target()?, base).map_err(Error::Uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
const BUFFER: &str = "31 gemini://geminiprotocol.net/path\r\n";
|
|
||||||
let bytes = BUFFER.as_bytes();
|
|
||||||
let base = Uri::build(
|
|
||||||
glib::UriFlags::NONE,
|
|
||||||
"gemini",
|
|
||||||
None,
|
|
||||||
Some("geminiprotocol.net"),
|
|
||||||
-1,
|
|
||||||
"/path/",
|
|
||||||
Some("query"),
|
|
||||||
Some("fragment"),
|
|
||||||
);
|
|
||||||
let permanent = Permanent::from_utf8(bytes).unwrap();
|
|
||||||
assert_eq!(permanent.as_str(), BUFFER);
|
|
||||||
assert_eq!(permanent.as_bytes(), bytes);
|
|
||||||
assert!(permanent.target().is_ok());
|
|
||||||
assert!(
|
|
||||||
permanent
|
|
||||||
.uri(&base)
|
|
||||||
.is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path")
|
|
||||||
);
|
|
||||||
assert!(Permanent::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
TargetEmpty,
|
|
||||||
Uri(super::super::UriError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::TargetEmpty => {
|
|
||||||
write!(f, "Expected target is empty")
|
|
||||||
}
|
|
||||||
Self::Uri(e) => {
|
|
||||||
write!(f, "URI parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
// Local dependencies
|
|
||||||
|
|
||||||
use glib::Uri;
|
|
||||||
|
|
||||||
/// [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code
|
|
||||||
pub const CODE: &[u8] = b"30";
|
|
||||||
|
|
||||||
/// Hold header `String` for [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code
|
|
||||||
/// * this response type does not contain body data
|
|
||||||
/// * the header member is closed to require valid construction
|
|
||||||
pub struct Temporary(String);
|
|
||||||
|
|
||||||
impl Temporary {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get raw target for `Self`
|
|
||||||
/// * return `Err` if the required target is empty
|
|
||||||
pub fn target(&self) -> Result<&str, Error> {
|
|
||||||
self.0
|
|
||||||
.get(2..)
|
|
||||||
.map(|s| s.trim())
|
|
||||||
.filter(|x| !x.is_empty())
|
|
||||||
.ok_or(Error::TargetEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uri(&self, base: &Uri) -> Result<Uri, Error> {
|
|
||||||
super::uri(self.target()?, base).map_err(Error::Uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
const BUFFER: &str = "30 gemini://geminiprotocol.net/path\r\n";
|
|
||||||
let bytes = BUFFER.as_bytes();
|
|
||||||
let base = Uri::build(
|
|
||||||
glib::UriFlags::NONE,
|
|
||||||
"gemini",
|
|
||||||
None,
|
|
||||||
Some("geminiprotocol.net"),
|
|
||||||
-1,
|
|
||||||
"/path/",
|
|
||||||
Some("query"),
|
|
||||||
Some("fragment"),
|
|
||||||
);
|
|
||||||
let temporary = Temporary::from_utf8(BUFFER.as_bytes()).unwrap();
|
|
||||||
assert_eq!(temporary.as_str(), BUFFER);
|
|
||||||
assert_eq!(temporary.as_bytes(), bytes);
|
|
||||||
assert!(temporary.target().is_ok());
|
|
||||||
assert!(
|
|
||||||
temporary
|
|
||||||
.uri(&base)
|
|
||||||
.is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path")
|
|
||||||
);
|
|
||||||
assert!(Temporary::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err())
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
TargetEmpty,
|
|
||||||
Uri(super::super::UriError),
|
|
||||||
Utf8Error(std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::TargetEmpty => {
|
|
||||||
write!(f, "Expected target is empty")
|
|
||||||
}
|
|
||||||
Self::Uri(e) => {
|
|
||||||
write!(f, "URI parse error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 decode error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
pub mod default;
|
|
||||||
pub mod error;
|
|
||||||
|
|
||||||
pub use default::Default;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
const CODE: u8 = b'2';
|
|
||||||
|
|
||||||
pub enum Success {
|
|
||||||
Default(Default),
|
|
||||||
// reserved for 2* codes
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Success {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse new `Self` from buffer bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if buffer.first().is_none_or(|b| *b != CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
match Default::from_utf8(buffer) {
|
|
||||||
Ok(default) => Ok(Self::Default(default)),
|
|
||||||
Err(e) => Err(Error::Default(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get header bytes for `Self` type
|
|
||||||
pub fn as_header_bytes(&self) -> &[u8] {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.header.as_bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string for `Self` type
|
|
||||||
pub fn as_header_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default.header.as_str(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get parsed MIME for `Self` type
|
|
||||||
///
|
|
||||||
/// * high-level method, useful to skip extra match case constructions;
|
|
||||||
/// * at this moment, Gemini protocol has only one status code in this scope,\
|
|
||||||
/// this method would be deprecated in future, use on your own risk!
|
|
||||||
pub fn mime(&self) -> Result<String, Error> {
|
|
||||||
match self {
|
|
||||||
Self::Default(default) => default
|
|
||||||
.header
|
|
||||||
.mime()
|
|
||||||
.map_err(|e| Error::Default(default::Error::Header(e))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
let r = "20 text/gemini; charset=utf-8; lang=en\r\n";
|
|
||||||
let b = r.as_bytes();
|
|
||||||
let s = Success::from_utf8(b).unwrap();
|
|
||||||
|
|
||||||
match s {
|
|
||||||
Success::Default(ref d) => {
|
|
||||||
assert_eq!(d.header.mime().unwrap(), "text/gemini");
|
|
||||||
assert!(d.content.is_empty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(s.as_header_bytes(), b);
|
|
||||||
assert_eq!(s.as_header_str(), r);
|
|
||||||
assert_eq!(s.mime().unwrap(), "text/gemini");
|
|
||||||
|
|
||||||
assert!(Success::from_utf8("40 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err())
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub mod header;
|
|
||||||
|
|
||||||
pub use error::Error;
|
|
||||||
pub use header::Header;
|
|
||||||
|
|
||||||
/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code
|
|
||||||
pub const CODE: &[u8] = b"20";
|
|
||||||
|
|
||||||
/// Holder for [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code
|
|
||||||
/// * this response type MAY contain body data
|
|
||||||
/// * the header has closed members to require valid construction
|
|
||||||
pub struct Default {
|
|
||||||
/// Formatted header holder with additional API
|
|
||||||
pub header: Header,
|
|
||||||
/// Default success response MAY include body data
|
|
||||||
/// * if the `Request` constructed with `Mode::HeaderOnly` flag,\
|
|
||||||
/// this value wants to be processed manually, using external application logic (specific for content-type)
|
|
||||||
pub content: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
let header = Header::from_utf8(buffer).map_err(Error::Header)?;
|
|
||||||
Ok(Self {
|
|
||||||
content: buffer
|
|
||||||
.get(header.as_bytes().len()..)
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.map_or(Vec::new(), |v| v.to_vec()),
|
|
||||||
header,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
let d = Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap();
|
|
||||||
assert_eq!(d.header.mime().unwrap(), "text/gemini");
|
|
||||||
assert!(d.content.is_empty());
|
|
||||||
|
|
||||||
let d =
|
|
||||||
Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\ndata".as_bytes()).unwrap();
|
|
||||||
assert_eq!(d.header.mime().unwrap(), "text/gemini");
|
|
||||||
assert_eq!(d.content.len(), 4);
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Header(super::header::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
pub struct Header(String);
|
|
||||||
|
|
||||||
impl Header {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse `Self` from buffer contains header bytes
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
if !buffer.starts_with(super::CODE) {
|
|
||||||
return Err(Error::Code);
|
|
||||||
}
|
|
||||||
Ok(Self(
|
|
||||||
std::str::from_utf8(
|
|
||||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
|
||||||
)
|
|
||||||
.map_err(Error::Utf8Error)?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Parse content type for `Self`
|
|
||||||
pub fn mime(&self) -> Result<String, Error> {
|
|
||||||
glib::Regex::split_simple(
|
|
||||||
r"^\d{2}\s([^\/]+\/[^\s;]+)",
|
|
||||||
&self.0,
|
|
||||||
glib::RegexCompileFlags::DEFAULT,
|
|
||||||
glib::RegexMatchFlags::DEFAULT,
|
|
||||||
)
|
|
||||||
.get(1)
|
|
||||||
.map(|s| s.trim())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.map_or(Err(Error::Mime), |s| Ok(s.to_lowercase()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header bytes of `Self`
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
self.0.as_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get header string of `Self`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
self.0.as_str()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
let s = "20 text/gemini; charset=utf-8; lang=en\r\n";
|
|
||||||
let b = s.as_bytes();
|
|
||||||
let h = Header::from_utf8(b).unwrap();
|
|
||||||
assert_eq!(h.mime().unwrap(), "text/gemini");
|
|
||||||
assert_eq!(h.as_bytes(), b);
|
|
||||||
assert_eq!(h.as_str(), s);
|
|
||||||
|
|
||||||
assert!(Header::from_utf8("21 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
use std::{
|
|
||||||
fmt::{Display, Formatter, Result},
|
|
||||||
str::Utf8Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Mime,
|
|
||||||
Header(crate::client::connection::response::HeaderBytesError),
|
|
||||||
Utf8Error(Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Mime => {
|
|
||||||
write!(f, "Unexpected content type")
|
|
||||||
}
|
|
||||||
Self::Header(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
Self::Utf8Error(e) => {
|
|
||||||
write!(f, "UTF-8 error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Code,
|
|
||||||
Default(super::default::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Code => {
|
|
||||||
write!(f, "Unexpected status code")
|
|
||||||
}
|
|
||||||
Self::Default(e) => {
|
|
||||||
write!(f, "Header error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +1,8 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Connect(gio::NetworkAddress, glib::Error),
|
Close,
|
||||||
Connection(gio::SocketConnection, crate::client::connection::Error),
|
Connect,
|
||||||
NetworkAddress(crate::client::connection::request::Error),
|
Input,
|
||||||
Request(
|
Output,
|
||||||
crate::client::connection::Connection,
|
Response,
|
||||||
crate::client::connection::Error,
|
BufferOverflow,
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Connect(_, e) => {
|
|
||||||
write!(f, "Connect error: {e}")
|
|
||||||
}
|
|
||||||
Self::Connection(_, e) => {
|
|
||||||
write!(f, "Connection init error: {e}")
|
|
||||||
}
|
|
||||||
Self::NetworkAddress(e) => {
|
|
||||||
write!(f, "Network address error: {e}")
|
|
||||||
}
|
|
||||||
Self::Request(_, e) => {
|
|
||||||
write!(f, "Connection error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
src/client/response.rs
Normal file
42
src/client/response.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
pub mod body;
|
||||||
|
pub mod error;
|
||||||
|
pub mod header;
|
||||||
|
|
||||||
|
pub use body::Body;
|
||||||
|
pub use error::Error;
|
||||||
|
pub use header::Header;
|
||||||
|
|
||||||
|
pub struct Response {
|
||||||
|
header: Header,
|
||||||
|
body: Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Create new `client::Response`
|
||||||
|
pub fn new(header: Header, body: Body) -> Self {
|
||||||
|
Self { header, body }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new `client::Response` from UTF-8 buffer
|
||||||
|
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
|
let header = match Header::from_response(buffer) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => return Err(Error::Header),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = match Body::from_response(buffer) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/client/response/body.rs
Normal file
46
src/client/response/body.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
use glib::GString;
|
||||||
|
|
||||||
|
pub struct Body {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Body {
|
||||||
|
/// Construct from response buffer
|
||||||
|
pub fn from_response(response: &[u8] /* @TODO */) -> Result<Self, Error> {
|
||||||
|
let start = Self::start(response)?;
|
||||||
|
|
||||||
|
let buffer = match response.get(start..) {
|
||||||
|
Some(result) => result,
|
||||||
|
None => return Err(Error::Buffer),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
buffer: Vec::from(buffer),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
pub fn buffer(&self) -> &Vec<u8> {
|
||||||
|
&self.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_gstring(&self) -> Result<GString, Error> {
|
||||||
|
match GString::from_utf8(self.buffer.to_vec()) {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/client/response/body/error.rs
Normal file
6
src/client/response/body/error.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub enum Error {
|
||||||
|
Buffer,
|
||||||
|
Decode,
|
||||||
|
Format,
|
||||||
|
Status,
|
||||||
|
}
|
||||||
4
src/client/response/error.rs
Normal file
4
src/client/response/error.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub enum Error {
|
||||||
|
Header,
|
||||||
|
Body,
|
||||||
|
}
|
||||||
70
src/client/response/header.rs
Normal file
70
src/client/response/header.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
pub struct Header {
|
||||||
|
status: Option<Status>,
|
||||||
|
meta: Option<Meta>,
|
||||||
|
mime: Option<Mime>,
|
||||||
|
// @TODO
|
||||||
|
// charset: Option<Charset>,
|
||||||
|
// language: Option<Language>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
/// Construct from response buffer
|
||||||
|
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
|
||||||
|
pub fn from_response(response: &[u8] /* @TODO */) -> Result<Self, Error> {
|
||||||
|
let end = Self::end(response)?;
|
||||||
|
|
||||||
|
let buffer = match response.get(..end) {
|
||||||
|
Some(result) => result,
|
||||||
|
None => return Err(Error::Buffer),
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = match Meta::from_header(buffer) {
|
||||||
|
Ok(result) => Some(result),
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mime = mime::from_header(buffer); // optional
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { status, meta, mime })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
pub fn status(&self) -> &Option<Status> {
|
||||||
|
&self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mime(&self) -> &Option<Mime> {
|
||||||
|
&self.mime
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn meta(&self) -> &Option<Meta> {
|
||||||
|
&self.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
fn end(buffer: &[u8]) -> Result<usize, Error> {
|
||||||
|
for (offset, &byte) in buffer.iter().enumerate() {
|
||||||
|
if byte == b'\r' {
|
||||||
|
return Ok(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::Format)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/client/response/header/charset.rs
Normal file
1
src/client/response/header/charset.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
// @TODO
|
||||||
5
src/client/response/header/error.rs
Normal file
5
src/client/response/header/error.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub enum Error {
|
||||||
|
Buffer,
|
||||||
|
Format,
|
||||||
|
Status,
|
||||||
|
}
|
||||||
1
src/client/response/header/language.rs
Normal file
1
src/client/response/header/language.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
// @TODO
|
||||||
26
src/client/response/header/meta.rs
Normal file
26
src/client/response/header/meta.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
use glib::GString;
|
||||||
|
|
||||||
|
pub struct Meta {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Meta {
|
||||||
|
pub fn from_header(buffer: &[u8] /* @TODO */) -> Result<Self, Error> {
|
||||||
|
let buffer = match buffer.get(2..) {
|
||||||
|
Some(bytes) => bytes.to_vec(),
|
||||||
|
None => return Err(Error::Undefined),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { buffer })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_gstring(&self) -> Result<GString, Error> {
|
||||||
|
match GString::from_utf8(self.buffer.clone()) {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(_) => Err(Error::Undefined),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/client/response/header/meta/error.rs
Normal file
4
src/client/response/header/meta/error.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub enum Error {
|
||||||
|
Decode,
|
||||||
|
Undefined,
|
||||||
|
}
|
||||||
62
src/client/response/header/mime.rs
Normal file
62
src/client/response/header/mime.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
use glib::{GString, Uri};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub enum Mime {
|
||||||
|
TextGemini,
|
||||||
|
TextPlain,
|
||||||
|
ImagePng,
|
||||||
|
ImageGif,
|
||||||
|
ImageJpeg,
|
||||||
|
ImageWebp,
|
||||||
|
} // @TODO
|
||||||
|
|
||||||
|
pub fn from_header(buffer: &[u8] /* @TODO */) -> Option<Mime> {
|
||||||
|
from_string(&match GString::from_utf8(buffer.to_vec()) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => return None, // @TODO error handler?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_path(path: &Path) -> Option<Mime> {
|
||||||
|
match path.extension().and_then(|extension| extension.to_str()) {
|
||||||
|
Some("gmi") | Some("gemini") => Some(Mime::TextGemini),
|
||||||
|
Some("txt") => Some(Mime::TextPlain),
|
||||||
|
Some("png") => Some(Mime::ImagePng),
|
||||||
|
Some("gif") => Some(Mime::ImageGif),
|
||||||
|
Some("jpeg") | Some("jpg") => Some(Mime::ImageJpeg),
|
||||||
|
Some("webp") => Some(Mime::ImageWebp),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_string(value: &str) -> Option<Mime> {
|
||||||
|
if value.contains("text/gemini") {
|
||||||
|
return Some(Mime::TextGemini);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.contains("text/plain") {
|
||||||
|
return Some(Mime::TextPlain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.contains("image/gif") {
|
||||||
|
return Some(Mime::ImageGif);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.contains("image/jpeg") {
|
||||||
|
return Some(Mime::ImageJpeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.contains("image/webp") {
|
||||||
|
return Some(Mime::ImageWebp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.contains("image/png") {
|
||||||
|
return Some(Mime::ImagePng);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_uri(uri: &Uri) -> Option<Mime> {
|
||||||
|
from_path(Path::new(&uri.to_string()))
|
||||||
|
}
|
||||||
31
src/client/response/header/status.rs
Normal file
31
src/client/response/header/status.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
use glib::GString;
|
||||||
|
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
|
||||||
|
pub enum Status {
|
||||||
|
Input,
|
||||||
|
SensitiveInput,
|
||||||
|
Success,
|
||||||
|
Redirect,
|
||||||
|
} // @TODO
|
||||||
|
|
||||||
|
pub fn from_header(buffer: &[u8] /* @TODO */) -> Result<Status, Error> {
|
||||||
|
match buffer.get(0..2) {
|
||||||
|
Some(bytes) => match GString::from_utf8(bytes.to_vec()) {
|
||||||
|
Ok(string) => from_string(string.as_str()),
|
||||||
|
Err(_) => Err(Error::Decode),
|
||||||
|
},
|
||||||
|
None => Err(Error::Undefined),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_string(code: &str) -> Result<Status, Error> {
|
||||||
|
match code {
|
||||||
|
"10" => Ok(Status::Input),
|
||||||
|
"11" => Ok(Status::SensitiveInput),
|
||||||
|
"20" => Ok(Status::Success),
|
||||||
|
_ => Err(Error::Undefined),
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/client/response/header/status/error.rs
Normal file
4
src/client/response/header/status/error.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub enum Error {
|
||||||
|
Decode,
|
||||||
|
Undefined,
|
||||||
|
}
|
||||||
23
src/client/socket.rs
Normal file
23
src/client/socket.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use gio::{prelude::SocketClientExt, SocketClient, SocketProtocol, TlsCertificateFlags};
|
||||||
|
|
||||||
|
pub struct Socket {
|
||||||
|
gobject: SocketClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Socket {
|
||||||
|
/// Create new `gio::SocketClient` preset for Gemini Protocol
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let gobject = SocketClient::new();
|
||||||
|
|
||||||
|
gobject.set_protocol(SocketProtocol::Tcp);
|
||||||
|
gobject.set_tls_validation_flags(TlsCertificateFlags::INSECURE);
|
||||||
|
gobject.set_tls(true);
|
||||||
|
|
||||||
|
Self { gobject }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return ref to `gio::SocketClient` GObject
|
||||||
|
pub fn gobject(&self) -> &SocketClient {
|
||||||
|
self.gobject.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod file_output_stream;
|
|
||||||
pub mod memory_input_stream;
|
|
||||||
pub mod network_address;
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub mod size;
|
|
||||||
|
|
||||||
pub use error::Error;
|
|
||||||
pub use size::Size;
|
|
||||||
|
|
||||||
use gio::{
|
|
||||||
Cancellable, FileOutputStream, IOStream,
|
|
||||||
prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual},
|
|
||||||
};
|
|
||||||
use glib::{Bytes, Priority, object::IsA};
|
|
||||||
|
|
||||||
/// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
/// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html)
|
|
||||||
/// * require `IOStream` reference to keep `Connection` active in async thread
|
|
||||||
pub fn from_stream_async(
|
|
||||||
io_stream: impl IsA<IOStream>,
|
|
||||||
file_output_stream: FileOutputStream,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
priority: Priority,
|
|
||||||
mut size: Size,
|
|
||||||
(on_chunk, on_complete): (
|
|
||||||
impl Fn(Bytes, usize) + 'static, // on_chunk
|
|
||||||
impl FnOnce(Result<(FileOutputStream, usize), Error>) + 'static, // on_complete
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
io_stream.input_stream().read_bytes_async(
|
|
||||||
size.chunk,
|
|
||||||
priority,
|
|
||||||
Some(&cancellable.clone()),
|
|
||||||
move |result| match result {
|
|
||||||
Ok(bytes) => {
|
|
||||||
size.total += bytes.len();
|
|
||||||
on_chunk(bytes.clone(), size.total);
|
|
||||||
|
|
||||||
if let Some(limit) = size.limit
|
|
||||||
&& size.total > limit
|
|
||||||
{
|
|
||||||
return on_complete(Err(Error::BytesTotal(size.total, limit)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.is_empty() {
|
|
||||||
return on_complete(Ok((file_output_stream, size.total)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure **all bytes** sent to the destination
|
|
||||||
// > A partial write is performed with the size of a message block, which is 16kB
|
|
||||||
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes
|
|
||||||
file_output_stream.clone().write_all_async(
|
|
||||||
bytes,
|
|
||||||
priority,
|
|
||||||
Some(&cancellable.clone()),
|
|
||||||
move |result| match result {
|
|
||||||
Ok(_) => from_stream_async(
|
|
||||||
io_stream,
|
|
||||||
file_output_stream,
|
|
||||||
cancellable,
|
|
||||||
priority,
|
|
||||||
size,
|
|
||||||
(on_chunk, on_complete),
|
|
||||||
),
|
|
||||||
Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Err(e) => on_complete(Err(Error::InputStream(e))),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
BytesTotal(usize, usize),
|
|
||||||
InputStream(glib::Error),
|
|
||||||
OutputStream(glib::Bytes, glib::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::BytesTotal(total, limit) => {
|
|
||||||
write!(f, "Bytes total limit reached: {total} / {limit}")
|
|
||||||
}
|
|
||||||
Self::InputStream(e) => {
|
|
||||||
write!(f, "Input stream error: {e}")
|
|
||||||
}
|
|
||||||
Self::OutputStream(_, e) => {
|
|
||||||
write!(f, "Output stream error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
/// Mutable bytes count
|
|
||||||
pub struct Size {
|
|
||||||
pub chunk: usize,
|
|
||||||
/// `None` for unlimited
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
pub total: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Size {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
chunk: 0x10000, // 64KB
|
|
||||||
limit: None,
|
|
||||||
total: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub mod size;
|
|
||||||
|
|
||||||
pub use error::Error;
|
|
||||||
pub use size::Size;
|
|
||||||
|
|
||||||
use gio::{
|
|
||||||
Cancellable, IOStream, MemoryInputStream,
|
|
||||||
prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt},
|
|
||||||
};
|
|
||||||
use glib::{Priority, object::IsA};
|
|
||||||
|
|
||||||
/// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
|
|
||||||
/// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
///
|
|
||||||
/// **Useful for**
|
|
||||||
/// * safe read (of memory overflow) to dynamically allocated buffer, where final size of target data unknown
|
|
||||||
/// * calculate bytes processed on chunk load
|
|
||||||
pub fn from_stream_async(
|
|
||||||
io_stream: impl IsA<IOStream>,
|
|
||||||
priority: Priority,
|
|
||||||
cancelable: Cancellable,
|
|
||||||
size: Size,
|
|
||||||
(on_chunk, on_complete): (
|
|
||||||
impl Fn(usize, usize) + 'static,
|
|
||||||
impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
for_memory_input_stream_async(
|
|
||||||
MemoryInputStream::new(),
|
|
||||||
io_stream,
|
|
||||||
priority,
|
|
||||||
cancelable,
|
|
||||||
size,
|
|
||||||
(on_chunk, on_complete),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
/// to [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
|
|
||||||
/// * require `IOStream` reference to keep `Connection` active in async thread
|
|
||||||
pub fn for_memory_input_stream_async(
|
|
||||||
memory_input_stream: MemoryInputStream,
|
|
||||||
io_stream: impl IsA<IOStream>,
|
|
||||||
priority: Priority,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
mut size: Size,
|
|
||||||
(on_chunk, on_complete): (
|
|
||||||
impl Fn(usize, usize) + 'static,
|
|
||||||
impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
io_stream.input_stream().read_bytes_async(
|
|
||||||
size.chunk,
|
|
||||||
priority,
|
|
||||||
Some(&cancellable.clone()),
|
|
||||||
move |result| match result {
|
|
||||||
Ok(bytes) => {
|
|
||||||
let len = bytes.len(); // calculate once
|
|
||||||
|
|
||||||
// is end of stream
|
|
||||||
if len == 0 {
|
|
||||||
return on_complete(Ok((memory_input_stream, size.total)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// callback chunk function
|
|
||||||
size.total += len;
|
|
||||||
on_chunk(len, size.total);
|
|
||||||
|
|
||||||
// push bytes into the memory pool
|
|
||||||
memory_input_stream.add_bytes(&bytes);
|
|
||||||
|
|
||||||
// prevent memory overflow
|
|
||||||
if size.total > size.limit {
|
|
||||||
return on_complete(Err(Error::BytesTotal(
|
|
||||||
memory_input_stream,
|
|
||||||
size.total,
|
|
||||||
size.limit,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle next chunk..
|
|
||||||
for_memory_input_stream_async(
|
|
||||||
memory_input_stream,
|
|
||||||
io_stream,
|
|
||||||
priority,
|
|
||||||
cancellable,
|
|
||||||
size,
|
|
||||||
(on_chunk, on_complete),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Err(e) => on_complete(Err(Error::InputStream(memory_input_stream, e))),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
BytesTotal(gio::MemoryInputStream, usize, usize),
|
|
||||||
InputStream(gio::MemoryInputStream, glib::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::BytesTotal(_, total, limit) => {
|
|
||||||
write!(f, "Bytes total limit reached: {total} / {limit}")
|
|
||||||
}
|
|
||||||
Self::InputStream(_, e) => {
|
|
||||||
write!(f, "Input stream error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
/// Mutable bytes count
|
|
||||||
pub struct Size {
|
|
||||||
pub chunk: usize,
|
|
||||||
pub limit: usize,
|
|
||||||
pub total: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Size {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
chunk: 0x10000, // 64KB
|
|
||||||
limit: 0xfffff, // 1 MB
|
|
||||||
total: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
use gio::NetworkAddress;
|
|
||||||
use glib::Uri;
|
|
||||||
|
|
||||||
/// Create new valid [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
|
||||||
///
|
|
||||||
/// Useful as:
|
|
||||||
/// * shared [SocketConnectable](https://docs.gtk.org/gio/iface.SocketConnectable.html) interface
|
|
||||||
/// * [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) record for TLS connections
|
|
||||||
pub fn from_uri(uri: &Uri, default_port: u16) -> Result<NetworkAddress, Error> {
|
|
||||||
Ok(NetworkAddress::new(
|
|
||||||
&match uri.host() {
|
|
||||||
Some(host) => host,
|
|
||||||
None => return Err(Error::Host(uri.to_string())),
|
|
||||||
},
|
|
||||||
if uri.port().is_positive() {
|
|
||||||
uri.port() as u16
|
|
||||||
} else {
|
|
||||||
default_port
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Host(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Host(url) => {
|
|
||||||
write!(f, "Host required for {url}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
src/lib.rs
17
src/lib.rs
|
|
@ -1,18 +1 @@
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod gio;
|
|
||||||
|
|
||||||
// Main API
|
|
||||||
|
|
||||||
pub use client::Client;
|
|
||||||
|
|
||||||
// Global defaults
|
|
||||||
|
|
||||||
pub const DEFAULT_PORT: u16 = 1965;
|
|
||||||
|
|
||||||
// Debug
|
|
||||||
|
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
pub const VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
|
|
||||||
pub const VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
|
|
||||||
pub const VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue