mirror of
https://github.com/YGGverse/ggemini.git
synced 2026-03-31 17:15:31 +00:00
Compare commits
293 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11d17e004e | ||
|
|
bba51e38e8 | ||
|
|
0f6eaa563c | ||
|
|
7e9ecf64b3 | ||
|
|
f8537e4ab6 | ||
|
|
5019e66667 | ||
|
|
d8e0a8e35a | ||
|
|
c5d10e020a | ||
|
|
e878fe4ba2 | ||
|
|
cc1018224a | ||
|
|
44196608ce | ||
|
|
bb5b1dfb53 | ||
|
|
c79f386bf1 | ||
|
|
9bbaecf344 | ||
|
|
4dddbd5f8a | ||
|
|
b6ea830545 | ||
|
|
8ee088270f | ||
|
|
46da3a031a | ||
|
|
ea1fb8ea66 | ||
|
|
c9a59e76ee | ||
|
|
e96ff688b3 | ||
|
|
0c75da793f | ||
|
|
064c4107f3 | ||
|
|
d565d56c17 | ||
|
|
3b24625d66 | ||
|
|
f513747e86 | ||
|
|
5229cdae85 | ||
|
|
473ed48715 | ||
|
|
4eb998ef20 | ||
|
|
232531a0bc | ||
|
|
1b96270598 | ||
|
|
845f3dc77e | ||
|
|
e94923ecb5 | ||
|
|
a32eccf5cb | ||
|
|
161142c809 | ||
|
|
8feab6b93b | ||
|
|
5360c6bf19 | ||
|
|
68277f8e83 | ||
|
|
71043bbf73 | ||
|
|
3de096cced | ||
|
|
68e7894125 | ||
|
|
0717e473b7 | ||
|
|
7c518cecf6 | ||
|
|
a12a73d311 | ||
|
|
2102d8887a | ||
|
|
9eb21bb6a3 | ||
|
|
3f968d87b1 | ||
|
|
ab8eb402a8 | ||
|
|
6dbf49cea3 | ||
|
|
b62f990bf2 | ||
|
|
376473660f | ||
|
|
5bb52fbd8c | ||
|
|
af8a972cca | ||
|
|
0aeb501760 | ||
|
|
e6661c1d00 | ||
|
|
fc8356f7ac | ||
|
|
90cc58ab92 | ||
|
|
d7166dac66 | ||
|
|
06fc69cff8 | ||
|
|
0523f67850 | ||
|
|
e3abd89c9d | ||
|
|
564f5b69d5 | ||
|
|
1ff38ee838 | ||
|
|
4f6799a495 | ||
|
|
f51c636401 | ||
|
|
e635c41065 | ||
|
|
862ab1ccfa | ||
|
|
9ce509cedc | ||
|
|
8334d8a83c | ||
|
|
0f1caadc03 | ||
|
|
83b29c0276 | ||
|
|
582744f830 | ||
|
|
bb8c2273d4 | ||
|
|
62f53304aa | ||
|
|
d4f076f074 | ||
|
|
b3e3f2e07b | ||
|
|
a953601141 | ||
|
|
867945ec74 | ||
|
|
46483d1829 | ||
|
|
4ee92645ca | ||
|
|
1505b6311c | ||
|
|
c9d5e5987c | ||
|
|
998a4e97b4 | ||
|
|
041454d8df | ||
|
|
9bb926f243 | ||
|
|
d57d9fc7df | ||
|
|
7518101b55 | ||
|
|
517153656b | ||
|
|
dc2300b1c0 | ||
|
|
a5fbca2ace | ||
|
|
788b792167 | ||
|
|
8df7af44b5 | ||
|
|
5358e43697 | ||
|
|
cdac038135 | ||
|
|
eee87d66b4 | ||
|
|
016f82d586 | ||
|
|
6da4c2ed52 | ||
|
|
0cb5ff9cbc | ||
|
|
f669dc6b23 | ||
|
|
86af47ff49 | ||
|
|
aa44e2723d | ||
|
|
67989dba63 | ||
|
|
37d30d700c | ||
|
|
ee0216a1a0 | ||
|
|
caa61bb808 | ||
|
|
ea9d7e4c5d | ||
|
|
075b5605a0 | ||
|
|
5e52e74870 | ||
|
|
52141f3dca | ||
|
|
490a513ddf | ||
|
|
e96771b926 | ||
|
|
48c7676788 | ||
|
|
946ff485be | ||
|
|
cac544043e | ||
|
|
90bff09269 | ||
|
|
d999e64e02 | ||
|
|
b5be9dcc76 | ||
|
|
5dd78dd43d | ||
|
|
962558c123 | ||
|
|
edf9982933 | ||
|
|
c5aada49b4 | ||
|
|
b5e864e807 | ||
|
|
e456719e58 | ||
|
|
d3133f50f7 | ||
|
|
09b2c626a3 | ||
|
|
61fbab6dae | ||
|
|
c67593e5fd | ||
|
|
339f0bb1af | ||
|
|
ac53f73a60 | ||
|
|
df191c8e25 | ||
|
|
a9283770db | ||
|
|
f4c9b73925 | ||
|
|
ce5d3ac4d2 | ||
|
|
e2097138a9 | ||
|
|
eab1786918 | ||
|
|
1c0de40617 | ||
|
|
042e2a16ea | ||
|
|
fa02234cbd | ||
|
|
a2261601f6 | ||
|
|
f93fd035ef | ||
|
|
8492ea7db0 | ||
|
|
fecbbff18f | ||
|
|
007921f73f | ||
|
|
fbdc20fd13 | ||
|
|
74e5e4d976 | ||
|
|
54f2b81475 | ||
|
|
29b835411d | ||
|
|
66a0de6a8e | ||
|
|
e2bee95140 | ||
|
|
256939c3b4 | ||
|
|
87ccca5373 | ||
|
|
54fabd241b | ||
|
|
7e9c574d4a | ||
|
|
bdc2b50940 | ||
|
|
249199f780 | ||
|
|
2c88f12f2d | ||
|
|
76235da239 | ||
|
|
e76bc62d82 | ||
|
|
1911b0ad95 | ||
|
|
f33e53e51b | ||
|
|
eb32db3d3b | ||
|
|
8c298977f3 | ||
|
|
55ee734a0b | ||
|
|
059fa8f2d7 | ||
|
|
6ac26bad62 | ||
|
|
1f05ccc149 | ||
|
|
18806e2f38 | ||
|
|
f0f34dfdb2 | ||
|
|
128b5d68b6 | ||
|
|
3c627d6e4b | ||
|
|
7802ffad67 | ||
|
|
a77e4abf50 | ||
|
|
d313f900ba | ||
|
|
41d7d8e4f3 | ||
|
|
3fbd6ff3e3 | ||
|
|
ce19e94db9 | ||
|
|
94d63bd6de | ||
|
|
096bd1d862 | ||
|
|
11313eafb9 | ||
|
|
66245ef8dc | ||
|
|
e9d51697bd | ||
|
|
4c0fea0e99 | ||
|
|
1a8bd44841 | ||
|
|
193dbef087 | ||
|
|
3791cbc4d0 | ||
|
|
a63e05685c | ||
|
|
8947052718 | ||
|
|
e442a2880a | ||
|
|
8b6f2200f5 | ||
|
|
4767929050 | ||
|
|
70fc128c29 | ||
|
|
730af453f6 | ||
|
|
7d90d974a0 | ||
|
|
6330bbfd85 | ||
|
|
8f910672e2 | ||
|
|
2df9f36599 | ||
|
|
99583aa719 | ||
|
|
4038172735 | ||
|
|
0cc6d10017 | ||
|
|
98c6150f74 | ||
|
|
17b2fcaaae | ||
|
|
273dac139e | ||
|
|
3cc9fcd86b | ||
|
|
911fb13a69 | ||
|
|
fe22b84916 | ||
|
|
af4a55659b | ||
|
|
efc9b62786 | ||
|
|
cca3e4daa6 | ||
|
|
072e6c3e7c | ||
|
|
5e9e3aecd2 | ||
|
|
2157e55db9 | ||
|
|
ec34228e0f | ||
|
|
5b01a5af54 | ||
|
|
c61568a5d8 | ||
|
|
0f8b98c395 | ||
|
|
c12c57cc99 | ||
|
|
559e03f904 | ||
|
|
cdf35db0d6 | ||
|
|
6ee60e9d9d | ||
|
|
79f219ba76 | ||
|
|
fbfd7a2c67 | ||
|
|
a715e7632a | ||
|
|
36569da73b | ||
|
|
0ab6f97815 | ||
|
|
e86a556863 | ||
|
|
2e6cdb000b | ||
|
|
ed68c22010 | ||
|
|
c779ca3788 | ||
|
|
1dfaf68267 | ||
|
|
653960c1ab | ||
|
|
8618b31570 | ||
|
|
9e3176ca00 | ||
|
|
873489df29 | ||
|
|
a06e4e9eff | ||
|
|
809e54b887 | ||
|
|
c57ac0de83 | ||
|
|
16971f9321 | ||
|
|
911539eab9 | ||
|
|
71135fac58 | ||
|
|
745b8a6786 | ||
|
|
9f9e0e0ea3 | ||
|
|
d0a26f429b | ||
|
|
a0fa799163 | ||
|
|
ab9c7f4400 | ||
|
|
2db7d77f43 | ||
|
|
7db362aadc | ||
|
|
c3a76472d2 | ||
|
|
b3e9bf239c | ||
|
|
c4c173f6cf | ||
|
|
f4cb0c3bcc | ||
|
|
16ed3efef0 | ||
|
|
1c50fadfde | ||
|
|
e437d35acf | ||
|
|
2079bb1167 | ||
|
|
9ebd1e03f6 | ||
|
|
6c88eedd33 | ||
|
|
2e7df281a9 | ||
|
|
1fbcfcfff0 | ||
|
|
b1b35e059b | ||
|
|
c61164e666 | ||
|
|
8f2820b171 | ||
|
|
5e20e5a925 | ||
|
|
b9be89ca0f | ||
|
|
ffda3d27e3 | ||
|
|
4e712260ff | ||
|
|
9d240c4c37 | ||
|
|
c7f992e1b3 | ||
|
|
cc0625d920 | ||
|
|
cbaa4c0e81 | ||
|
|
017a63b4e5 | ||
|
|
5737b89278 | ||
|
|
239786da6a | ||
|
|
3a9e84a3d9 | ||
|
|
67d486cc4d | ||
|
|
32a879b756 | ||
|
|
af7aaa63e4 | ||
|
|
1d4dc50380 | ||
|
|
d6dc4d6870 | ||
|
|
c1c06667c9 | ||
|
|
7215258a71 | ||
|
|
03b2c36153 | ||
|
|
fab352e0b5 | ||
|
|
5aaea57304 | ||
|
|
6435a88cb6 | ||
|
|
6e46ddb676 | ||
|
|
cb56e614ae | ||
|
|
cd9b132952 | ||
|
|
cac4168645 | ||
|
|
c4fa868345 | ||
|
|
9dd1bcc9dd | ||
|
|
d34d24588f | ||
|
|
ac17a48144 | ||
|
|
b6134c92d5 |
90 changed files with 4068 additions and 724 deletions
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
custom: https://yggverse.github.io/#donate
|
||||
31
.github/workflows/build.yml
vendored
Normal file
31
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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
|
||||
11
Cargo.toml
11
Cargo.toml
|
|
@ -1,19 +1,20 @@
|
|||
[package]
|
||||
name = "ggemini"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
version = "0.20.1"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
description = "Glib/Gio-oriented network API for Gemini protocol"
|
||||
keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"]
|
||||
keywords = ["gemini", "titan", "glib", "gio", "client"]
|
||||
categories = ["development-tools", "network-programming", "parsing"]
|
||||
repository = "https://github.com/YGGverse/ggemini"
|
||||
|
||||
[dependencies.gio]
|
||||
package = "gio"
|
||||
version = "0.20.4"
|
||||
version = "0.21.0"
|
||||
features = ["v2_70"]
|
||||
|
||||
[dependencies.glib]
|
||||
package = "glib"
|
||||
version = "0.20.4"
|
||||
version = "0.21.0"
|
||||
features = ["v2_66"]
|
||||
|
|
|
|||
59
README.md
59
README.md
|
|
@ -1,13 +1,30 @@
|
|||
# ggemini
|
||||
|
||||

|
||||
[](https://docs.rs/ggemini)
|
||||
[](https://crates.io/crates/ggemini)
|
||||
|
||||
Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Project in development!
|
||||
>
|
||||
|
||||
GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol,
|
||||
it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only
|
||||
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
|
||||
|
||||
|
|
@ -19,6 +36,42 @@ cargo add ggemini
|
|||
|
||||
* [Documentation](https://docs.rs/ggemini/latest/ggemini/)
|
||||
|
||||
## See also
|
||||
### 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
|
||||
117
src/client.rs
117
src/client.rs
|
|
@ -1,4 +1,115 @@
|
|||
//! Client API to interact Server using
|
||||
//! [Gemini protocol](https://geminiprotocol.net/docs/protocol-specification.gmi)
|
||||
//! High-level client API to interact with Gemini Socket Server:
|
||||
//! * https://geminiprotocol.net/docs/protocol-specification.gmi
|
||||
|
||||
pub mod response;
|
||||
pub mod connection;
|
||||
pub mod error;
|
||||
|
||||
pub use connection::{Connection, Request, Response};
|
||||
pub use error::Error;
|
||||
|
||||
use gio::{Cancellable, SocketClient, SocketProtocol, TlsCertificate, prelude::SocketClientExt};
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
172
src/client/connection.rs
Normal file
172
src/client/connection.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
pub mod error;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use error::Error;
|
||||
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)),
|
||||
}
|
||||
}
|
||||
24
src/client/connection/error.rs
Normal file
24
src/client/connection/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/client/connection/request.rs
Normal file
122
src/client/connection/request.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
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(),
|
||||
)
|
||||
);
|
||||
}
|
||||
16
src/client/connection/request/error.rs
Normal file
16
src/client/connection/request/error.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/client/connection/request/mode.rs
Normal file
6
src/client/connection/request/mode.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/// 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,
|
||||
}
|
||||
146
src/client/connection/response.rs
Normal file
146
src/client/connection/response.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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)
|
||||
}
|
||||
111
src/client/connection/response/certificate.rs
Normal file
111
src/client/connection/response/certificate.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
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);
|
||||
}
|
||||
40
src/client/connection/response/certificate/error.rs
Normal file
40
src/client/connection/response/certificate/error.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/client/connection/response/certificate/not_authorized.rs
Normal file
79
src/client/connection/response/certificate/not_authorized.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/client/connection/response/certificate/not_valid.rs
Normal file
79
src/client/connection/response/certificate/not_valid.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/client/connection/response/certificate/required.rs
Normal file
79
src/client/connection/response/certificate/required.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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());
|
||||
}
|
||||
24
src/client/connection/response/certificate/required/error.rs
Normal file
24
src/client/connection/response/certificate/required/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/client/connection/response/error.rs
Normal file
70
src/client/connection/response/error.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/client/connection/response/failure.rs
Normal file
89
src/client/connection/response/failure.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
28
src/client/connection/response/failure/error.rs
Normal file
28
src/client/connection/response/failure/error.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/client/connection/response/failure/permanent.rs
Normal file
126
src/client/connection/response/failure/permanent.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/client/connection/response/failure/permanent/default.rs
Normal file
78
src/client/connection/response/failure/permanent/default.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/client/connection/response/failure/permanent/error.rs
Normal file
48
src/client/connection/response/failure/permanent/error.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/client/connection/response/failure/permanent/gone.rs
Normal file
78
src/client/connection/response/failure/permanent/gone.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/client/connection/response/failure/temporary.rs
Normal file
126
src/client/connection/response/failure/temporary.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/client/connection/response/failure/temporary/default.rs
Normal file
78
src/client/connection/response/failure/temporary/default.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/client/connection/response/failure/temporary/error.rs
Normal file
48
src/client/connection/response/failure/temporary/error.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/client/connection/response/input.rs
Normal file
92
src/client/connection/response/input.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
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);
|
||||
}
|
||||
78
src/client/connection/response/input/default.rs
Normal file
78
src/client/connection/response/input/default.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
24
src/client/connection/response/input/default/error.rs
Normal file
24
src/client/connection/response/input/default/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/client/connection/response/input/error.rs
Normal file
36
src/client/connection/response/input/error.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/client/connection/response/input/sensitive.rs
Normal file
78
src/client/connection/response/input/sensitive.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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());
|
||||
}
|
||||
24
src/client/connection/response/input/sensitive/error.rs
Normal file
24
src/client/connection/response/input/sensitive/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/client/connection/response/redirect.rs
Normal file
185
src/client/connection/response/redirect.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
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/");
|
||||
}
|
||||
56
src/client/connection/response/redirect/error.rs
Normal file
56
src/client/connection/response/redirect/error.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/client/connection/response/redirect/permanent.rs
Normal file
82
src/client/connection/response/redirect/permanent.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
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());
|
||||
}
|
||||
32
src/client/connection/response/redirect/permanent/error.rs
Normal file
32
src/client/connection/response/redirect/permanent/error.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/client/connection/response/redirect/temporary.rs
Normal file
82
src/client/connection/response/redirect/temporary.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
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())
|
||||
}
|
||||
32
src/client/connection/response/redirect/temporary/error.rs
Normal file
32
src/client/connection/response/redirect/temporary/error.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/client/connection/response/success.rs
Normal file
76
src/client/connection/response/success.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
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())
|
||||
}
|
||||
51
src/client/connection/response/success/default.rs
Normal file
51
src/client/connection/response/success/default.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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);
|
||||
}
|
||||
20
src/client/connection/response/success/default/error.rs
Normal file
20
src/client/connection/response/success/default/error.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/client/connection/response/success/default/header.rs
Normal file
60
src/client/connection/response/success/default/header.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/client/connection/response/success/error.rs
Normal file
20
src/client/connection/response/success/error.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/client/error.rs
Normal file
31
src/client/error.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Connect(gio::NetworkAddress, glib::Error),
|
||||
Connection(gio::SocketConnection, crate::client::connection::Error),
|
||||
NetworkAddress(crate::client::connection::request::Error),
|
||||
Request(
|
||||
crate::client::connection::Connection,
|
||||
crate::client::connection::Error,
|
||||
),
|
||||
}
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
//! Read and parse Gemini response as Object
|
||||
|
||||
pub mod data;
|
||||
pub mod meta;
|
||||
|
||||
pub use meta::Meta;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//! Gemini response could have different MIME type for data.
|
||||
//! Use one of components below to parse response according to content type expected.
|
||||
//!
|
||||
//! * MIME type could be detected using `client::response::Meta` parser
|
||||
|
||||
pub mod text;
|
||||
pub use text::Text;
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
//! Tools for Text-based response
|
||||
|
||||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
// Local dependencies
|
||||
use gio::{
|
||||
prelude::{IOStreamExt, InputStreamExt},
|
||||
Cancellable, SocketConnection,
|
||||
};
|
||||
use glib::{GString, Priority};
|
||||
|
||||
// Default limits
|
||||
pub const BUFFER_CAPACITY: usize = 0x400; // 1024
|
||||
pub const BUFFER_MAX_SIZE: usize = 0xfffff; // 1M
|
||||
|
||||
/// Container for text-based response data
|
||||
pub struct Text {
|
||||
data: GString,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: GString::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new `Self` from string
|
||||
pub fn from_string(data: &str) -> Self {
|
||||
Self { data: data.into() }
|
||||
}
|
||||
|
||||
/// Create new `Self` from UTF-8 buffer
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, (Error, Option<&str>)> {
|
||||
match GString::from_utf8(buffer.into()) {
|
||||
Ok(data) => Ok(Self::from_string(&data)),
|
||||
Err(_) => Err((Error::Decode, None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Asynchronously create new `Self` from [InputStream](https://docs.gtk.org/gio/class.InputStream.html)
|
||||
/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
||||
pub fn from_socket_connection_async(
|
||||
socket_connection: SocketConnection,
|
||||
priority: Option<Priority>,
|
||||
cancellable: Option<Cancellable>,
|
||||
on_complete: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
|
||||
) {
|
||||
read_all_from_socket_connection_async(
|
||||
Vec::with_capacity(BUFFER_CAPACITY),
|
||||
socket_connection,
|
||||
match cancellable {
|
||||
Some(value) => Some(value),
|
||||
None => None::<Cancellable>,
|
||||
},
|
||||
match priority {
|
||||
Some(value) => value,
|
||||
None => Priority::DEFAULT,
|
||||
},
|
||||
|result| match result {
|
||||
Ok(buffer) => on_complete(Self::from_utf8(&buffer)),
|
||||
Err(reason) => on_complete(Err(reason)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get reference to `Self` data
|
||||
pub fn data(&self) -> &GString {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
/// Asynchronously read all bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html)
|
||||
/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
||||
///
|
||||
/// Return UTF-8 buffer collected.
|
||||
///
|
||||
/// * this function implements low-level helper for `Text::from_socket_connection_async`, also provides public API for external integrations
|
||||
/// * requires `SocketConnection` instead of `InputStream` to keep connection alive (by increasing reference count in async context) @TODO
|
||||
pub fn read_all_from_socket_connection_async(
|
||||
mut buffer: Vec<u8>,
|
||||
socket_connection: SocketConnection,
|
||||
cancelable: Option<Cancellable>,
|
||||
priority: Priority,
|
||||
callback: impl FnOnce(Result<Vec<u8>, (Error, Option<&str>)>) + 'static,
|
||||
) {
|
||||
socket_connection.input_stream().read_bytes_async(
|
||||
BUFFER_CAPACITY,
|
||||
priority,
|
||||
cancelable.clone().as_ref(),
|
||||
move |result| match result {
|
||||
Ok(bytes) => {
|
||||
// No bytes were read, end of stream
|
||||
if bytes.len() == 0 {
|
||||
return callback(Ok(buffer));
|
||||
}
|
||||
|
||||
// Validate overflow
|
||||
if buffer.len() + bytes.len() > BUFFER_MAX_SIZE {
|
||||
return callback(Err((Error::BufferOverflow, None)));
|
||||
}
|
||||
|
||||
// Save chunks to buffer
|
||||
for &byte in bytes.iter() {
|
||||
buffer.push(byte);
|
||||
}
|
||||
|
||||
// Continue bytes reading
|
||||
read_all_from_socket_connection_async(
|
||||
buffer,
|
||||
socket_connection,
|
||||
cancelable,
|
||||
priority,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
BufferOverflow,
|
||||
Decode,
|
||||
InputStream,
|
||||
}
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
//! Components for reading and parsing meta bytes from response:
|
||||
//! * [Gemini status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
||||
//! * meta data (for interactive statuses like 10, 11, 30 etc)
|
||||
//! * MIME type
|
||||
|
||||
pub mod data;
|
||||
pub mod error;
|
||||
pub mod mime;
|
||||
pub mod status;
|
||||
|
||||
pub use data::Data;
|
||||
pub use error::Error;
|
||||
pub use mime::Mime;
|
||||
pub use status::Status;
|
||||
|
||||
use gio::{
|
||||
prelude::{IOStreamExt, InputStreamExtManual},
|
||||
Cancellable, SocketConnection,
|
||||
};
|
||||
use glib::Priority;
|
||||
|
||||
pub const MAX_LEN: usize = 0x400; // 1024
|
||||
|
||||
pub struct Meta {
|
||||
status: Status,
|
||||
data: Option<Data>,
|
||||
mime: Option<Mime>,
|
||||
// @TODO
|
||||
// charset: Option<Charset>,
|
||||
// language: Option<Language>,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self` from UTF-8 buffer
|
||||
/// * supports entire response or just meta slice
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, (Error, Option<&str>)> {
|
||||
// Calculate buffer length once
|
||||
let len = buffer.len();
|
||||
|
||||
// Parse meta bytes only
|
||||
match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) {
|
||||
Some(slice) => {
|
||||
// Parse data
|
||||
let data = Data::from_utf8(&slice);
|
||||
|
||||
if let Err(reason) = data {
|
||||
return Err((
|
||||
match reason {
|
||||
data::Error::Decode => Error::DataDecode,
|
||||
data::Error::Protocol => Error::DataProtocol,
|
||||
},
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
// MIME
|
||||
|
||||
let mime = Mime::from_utf8(&slice);
|
||||
|
||||
if let Err(reason) = mime {
|
||||
return Err((
|
||||
match reason {
|
||||
mime::Error::Decode => Error::MimeDecode,
|
||||
mime::Error::Protocol => Error::MimeProtocol,
|
||||
mime::Error::Undefined => Error::MimeUndefined,
|
||||
},
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
// Status
|
||||
|
||||
let status = Status::from_utf8(&slice);
|
||||
|
||||
if let Err(reason) = status {
|
||||
return Err((
|
||||
match reason {
|
||||
status::Error::Decode => Error::StatusDecode,
|
||||
status::Error::Protocol => Error::StatusProtocol,
|
||||
status::Error::Undefined => Error::StatusUndefined,
|
||||
},
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
data: data.unwrap(),
|
||||
mime: mime.unwrap(),
|
||||
status: status.unwrap(),
|
||||
})
|
||||
}
|
||||
None => Err((Error::Protocol, None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Asynchronously create new `Self` from [InputStream](https://docs.gtk.org/gio/class.InputStream.html)
|
||||
/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
||||
pub fn from_socket_connection_async(
|
||||
socket_connection: SocketConnection,
|
||||
priority: Option<Priority>,
|
||||
cancellable: Option<Cancellable>,
|
||||
on_complete: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
|
||||
) {
|
||||
read_from_socket_connection_async(
|
||||
Vec::with_capacity(MAX_LEN),
|
||||
socket_connection,
|
||||
match cancellable {
|
||||
Some(value) => Some(value),
|
||||
None => None::<Cancellable>,
|
||||
},
|
||||
match priority {
|
||||
Some(value) => value,
|
||||
None => Priority::DEFAULT,
|
||||
},
|
||||
|result| match result {
|
||||
Ok(buffer) => on_complete(Self::from_utf8(&buffer)),
|
||||
Err(reason) => on_complete(Err(reason)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
pub fn status(&self) -> &Status {
|
||||
&self.status
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &Option<Data> {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn mime(&self) -> &Option<Mime> {
|
||||
&self.mime
|
||||
}
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
/// Asynchronously read all meta bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html)
|
||||
/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
||||
///
|
||||
/// Return UTF-8 buffer collected.
|
||||
///
|
||||
/// * this function implements low-level helper for `Meta::from_socket_connection_async`, also provides public API for external integrations
|
||||
/// * requires `SocketConnection` instead of `InputStream` to keep connection alive (by increasing reference count in async context) @TODO
|
||||
pub fn read_from_socket_connection_async(
|
||||
mut buffer: Vec<u8>,
|
||||
connection: SocketConnection,
|
||||
cancellable: Option<Cancellable>,
|
||||
priority: Priority,
|
||||
on_complete: impl FnOnce(Result<Vec<u8>, (Error, Option<&str>)>) + 'static,
|
||||
) {
|
||||
connection.input_stream().read_async(
|
||||
vec![0],
|
||||
priority,
|
||||
cancellable.clone().as_ref(),
|
||||
move |result| match result {
|
||||
Ok((mut bytes, size)) => {
|
||||
// Expect valid header length
|
||||
if size == 0 || buffer.len() >= MAX_LEN {
|
||||
return on_complete(Err((Error::Protocol, None)));
|
||||
}
|
||||
|
||||
// Read next byte without record
|
||||
if bytes.contains(&b'\r') {
|
||||
return read_from_socket_connection_async(
|
||||
buffer,
|
||||
connection,
|
||||
cancellable,
|
||||
priority,
|
||||
on_complete,
|
||||
);
|
||||
}
|
||||
|
||||
// Complete without record
|
||||
if bytes.contains(&b'\n') {
|
||||
return on_complete(Ok(buffer));
|
||||
}
|
||||
|
||||
// Record
|
||||
buffer.append(&mut bytes);
|
||||
|
||||
// Continue
|
||||
read_from_socket_connection_async(
|
||||
buffer,
|
||||
connection,
|
||||
cancellable,
|
||||
priority,
|
||||
on_complete,
|
||||
);
|
||||
}
|
||||
Err((_, reason)) => on_complete(Err((Error::InputStream, Some(reason.message())))),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
// @TODO
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
//! Components for reading and parsing meta **data** bytes from response
|
||||
//! (e.g. placeholder text for 10, 11, url string for 30, 31 etc)
|
||||
|
||||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
use glib::GString;
|
||||
|
||||
/// Meta **data** holder
|
||||
///
|
||||
/// For example, `value` could contain:
|
||||
/// * placeholder text for 10, 11 status
|
||||
/// * URL string for 30, 31 status
|
||||
pub struct Data {
|
||||
value: GString,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
// Constructors
|
||||
|
||||
/// Parse meta **data** from UTF-8 buffer
|
||||
/// from entire response or just header slice
|
||||
///
|
||||
/// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
||||
/// that does not expect any data in header
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Option<Self>, Error> {
|
||||
// Define max buffer length for this method
|
||||
const MAX_LEN: usize = 0x400; // 1024
|
||||
|
||||
// Init bytes buffer
|
||||
let mut bytes: Vec<u8> = Vec::with_capacity(MAX_LEN);
|
||||
|
||||
// Calculate len once
|
||||
let len = buffer.len();
|
||||
|
||||
// Skip 3 bytes for status code of `MAX_LEN` expected
|
||||
match buffer.get(3..if len > MAX_LEN { MAX_LEN - 3 } else { len }) {
|
||||
Some(slice) => {
|
||||
for &byte in slice {
|
||||
// End of header
|
||||
if byte == b'\r' {
|
||||
break;
|
||||
}
|
||||
|
||||
// Continue
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
// Assumes the bytes are valid UTF-8
|
||||
match GString::from_utf8(bytes) {
|
||||
Ok(value) => Ok(match value.is_empty() {
|
||||
false => Some(Self { value }),
|
||||
true => None,
|
||||
}),
|
||||
Err(_) => Err(Error::Decode),
|
||||
}
|
||||
}
|
||||
None => Err(Error::Protocol),
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
pub fn value(&self) -> &GString {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Decode,
|
||||
Protocol,
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
DataDecode,
|
||||
DataProtocol,
|
||||
InputStream,
|
||||
MimeDecode,
|
||||
MimeProtocol,
|
||||
MimeUndefined,
|
||||
Protocol,
|
||||
StatusDecode,
|
||||
StatusProtocol,
|
||||
StatusUndefined,
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
// @TODO
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
//! MIME type parser for different data types:
|
||||
//!
|
||||
//! * UTF-8 buffer with entire response or just with meta slice (that include **header**)
|
||||
//! * String (that include **header**)
|
||||
//! * [Uri](https://docs.gtk.org/glib/struct.Uri.html) (that include **extension**)
|
||||
//! * `std::Path` (that include **extension**)
|
||||
|
||||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
use glib::{GString, Uri};
|
||||
use std::path::Path;
|
||||
|
||||
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
|
||||
#[derive(Debug)]
|
||||
pub enum Mime {
|
||||
// Text
|
||||
TextGemini,
|
||||
TextPlain,
|
||||
// Image
|
||||
ImageGif,
|
||||
ImageJpeg,
|
||||
ImagePng,
|
||||
ImageWebp,
|
||||
// Audio
|
||||
AudioFlac,
|
||||
AudioMpeg,
|
||||
AudioOgg,
|
||||
} // @TODO
|
||||
|
||||
impl Mime {
|
||||
/// Create new `Self` from UTF-8 buffer
|
||||
///
|
||||
/// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) that does not expect MIME type in header
|
||||
/// * includes `Self::from_string` parser, it means that given buffer should contain some **header** (not filepath or any other type of strings)
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Option<Self>, Error> {
|
||||
// Define max buffer length for this method
|
||||
const MAX_LEN: usize = 0x400; // 1024
|
||||
|
||||
// Calculate buffer length once
|
||||
let len = buffer.len();
|
||||
|
||||
// Parse meta bytes only
|
||||
match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) {
|
||||
Some(value) => match GString::from_utf8(value.into()) {
|
||||
Ok(string) => Self::from_string(string.as_str()),
|
||||
Err(_) => Err(Error::Decode),
|
||||
},
|
||||
None => Err(Error::Protocol),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new `Self` from `std::Path` that includes file **extension**
|
||||
pub fn from_path(path: &Path) -> Result<Self, Error> {
|
||||
match path.extension().and_then(|extension| extension.to_str()) {
|
||||
// Text
|
||||
Some("gmi" | "gemini") => Ok(Self::TextGemini),
|
||||
Some("txt") => Ok(Self::TextPlain),
|
||||
// Image
|
||||
Some("gif") => Ok(Self::ImageGif),
|
||||
Some("jpeg" | "jpg") => Ok(Self::ImageJpeg),
|
||||
Some("png") => Ok(Self::ImagePng),
|
||||
Some("webp") => Ok(Self::ImageWebp),
|
||||
// Audio
|
||||
Some("flac") => Ok(Self::AudioFlac),
|
||||
Some("mp3") => Ok(Self::AudioMpeg),
|
||||
Some("oga" | "ogg" | "opus" | "spx") => Ok(Self::AudioOgg),
|
||||
_ => Err(Error::Undefined),
|
||||
} // @TODO extension to lowercase
|
||||
}
|
||||
|
||||
/// Create new `Self` from string that includes **header**
|
||||
///
|
||||
/// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
||||
/// that does not expect MIME type
|
||||
pub fn from_string(value: &str) -> Result<Option<Self>, Error> {
|
||||
// Text
|
||||
if value.contains("text/gemini") {
|
||||
return Ok(Some(Self::TextGemini));
|
||||
}
|
||||
|
||||
if value.contains("text/plain") {
|
||||
return Ok(Some(Self::TextPlain));
|
||||
}
|
||||
|
||||
// Image
|
||||
if value.contains("image/gif") {
|
||||
return Ok(Some(Self::ImageGif));
|
||||
}
|
||||
|
||||
if value.contains("image/jpeg") {
|
||||
return Ok(Some(Self::ImageJpeg));
|
||||
}
|
||||
|
||||
if value.contains("image/webp") {
|
||||
return Ok(Some(Self::ImageWebp));
|
||||
}
|
||||
|
||||
if value.contains("image/png") {
|
||||
return Ok(Some(Self::ImagePng));
|
||||
}
|
||||
|
||||
// Audio
|
||||
if value.contains("audio/flac") {
|
||||
return Ok(Some(Self::AudioFlac));
|
||||
}
|
||||
|
||||
if value.contains("audio/mpeg") {
|
||||
return Ok(Some(Self::AudioMpeg));
|
||||
}
|
||||
|
||||
if value.contains("audio/ogg") {
|
||||
return Ok(Some(Self::AudioOgg));
|
||||
}
|
||||
|
||||
// Some type exist, but not defined yet
|
||||
if value.contains("/") {
|
||||
return Err(Error::Undefined);
|
||||
}
|
||||
|
||||
// Done
|
||||
Ok(None) // may be empty (for some status codes)
|
||||
}
|
||||
|
||||
/// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
||||
/// that includes file **extension**
|
||||
pub fn from_uri(uri: &Uri) -> Result<Self, Error> {
|
||||
Self::from_path(Path::new(&uri.to_string()))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Decode,
|
||||
Protocol,
|
||||
Undefined,
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
use glib::GString;
|
||||
|
||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
|
||||
#[derive(Debug)]
|
||||
pub enum Status {
|
||||
// 10 | 11
|
||||
Input,
|
||||
SensitiveInput,
|
||||
// 20
|
||||
Success,
|
||||
// 30 | 31
|
||||
Redirect,
|
||||
PermanentRedirect,
|
||||
} // @TODO
|
||||
|
||||
impl Status {
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
match buffer.get(0..2) {
|
||||
Some(value) => match GString::from_utf8(value.to_vec()) {
|
||||
Ok(string) => Self::from_string(string.as_str()),
|
||||
Err(_) => Err(Error::Decode),
|
||||
},
|
||||
None => Err(Error::Protocol),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(code: &str) -> Result<Self, Error> {
|
||||
match code {
|
||||
"10" => Ok(Self::Input),
|
||||
"11" => Ok(Self::SensitiveInput),
|
||||
"20" => Ok(Self::Success),
|
||||
"30" => Ok(Self::Redirect),
|
||||
"31" => Ok(Self::PermanentRedirect),
|
||||
_ => Err(Error::Undefined),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Decode,
|
||||
Protocol,
|
||||
Undefined,
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
pub mod file_output_stream;
|
||||
pub mod memory_input_stream;
|
||||
pub mod network_address;
|
||||
|
|
|
|||
69
src/gio/file_output_stream.rs
Normal file
69
src/gio/file_output_stream.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
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))),
|
||||
},
|
||||
)
|
||||
}
|
||||
24
src/gio/file_output_stream/error.rs
Normal file
24
src/gio/file_output_stream/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/gio/file_output_stream/size.rs
Normal file
17
src/gio/file_output_stream/size.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/// 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,130 +1,95 @@
|
|||
pub mod error;
|
||||
pub mod size;
|
||||
|
||||
pub use error::Error;
|
||||
pub use size::Size;
|
||||
|
||||
use gio::{
|
||||
Cancellable, IOStream, MemoryInputStream,
|
||||
prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt},
|
||||
Cancellable, MemoryInputStream, SocketConnection,
|
||||
};
|
||||
use glib::{Bytes, Priority};
|
||||
use glib::{Priority, object::IsA};
|
||||
|
||||
/// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
|
||||
/// from [InputStream](https://docs.gtk.org/gio/class.InputStream.html)
|
||||
/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
||||
/// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
||||
///
|
||||
/// Useful to create dynamically allocated, memory-safe buffer
|
||||
/// from remote connections, where final size of target data could not be known by Gemini protocol restrictions.
|
||||
/// Also, could be useful for [Pixbuf](https://docs.gtk.org/gdk-pixbuf/class.Pixbuf.html) or
|
||||
/// loading widgets like [Spinner](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Spinner.html)
|
||||
/// to display bytes on async data loading.
|
||||
///
|
||||
/// * this function takes entire `SocketConnection` reference (not `MemoryInputStream`) just to keep connection alive in the async context
|
||||
///
|
||||
/// **Implementation**
|
||||
///
|
||||
/// Implements low-level `read_all_from_socket_connection_async` function:
|
||||
/// * recursively read all bytes from `InputStream` for `SocketConnection` according to `bytes_in_chunk` argument
|
||||
/// * calculates total bytes length on every chunk iteration, validate sum with `bytes_total_limit` argument
|
||||
/// * stop reading `InputStream` with `Result` on zero bytes in chunk received
|
||||
/// * applies callback functions:
|
||||
/// * `on_chunk` - return reference to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) and `bytes_total` collected for every chunk in reading loop
|
||||
/// * `on_complete` - return `MemoryInputStream` on success or `Error` on failure as `Result`
|
||||
pub fn from_socket_connection_async(
|
||||
socket_connection: SocketConnection,
|
||||
cancelable: Option<Cancellable>,
|
||||
/// **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,
|
||||
bytes_in_chunk: usize,
|
||||
bytes_total_limit: usize,
|
||||
on_chunk: impl Fn((Bytes, usize)) + 'static,
|
||||
on_complete: impl FnOnce(Result<MemoryInputStream, (Error, Option<&str>)>) + 'static,
|
||||
cancelable: Cancellable,
|
||||
size: Size,
|
||||
(on_chunk, on_complete): (
|
||||
impl Fn(usize, usize) + 'static,
|
||||
impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static,
|
||||
),
|
||||
) {
|
||||
read_all_from_socket_connection_async(
|
||||
for_memory_input_stream_async(
|
||||
MemoryInputStream::new(),
|
||||
socket_connection,
|
||||
cancelable,
|
||||
io_stream,
|
||||
priority,
|
||||
bytes_in_chunk,
|
||||
bytes_total_limit,
|
||||
0, // initial `bytes_total` value
|
||||
on_chunk,
|
||||
on_complete,
|
||||
cancelable,
|
||||
size,
|
||||
(on_chunk, on_complete),
|
||||
);
|
||||
}
|
||||
|
||||
/// Low-level helper for `from_socket_connection_async` function,
|
||||
/// also provides public API for external usage.
|
||||
///
|
||||
/// Asynchronously read [InputStream](https://docs.gtk.org/gio/class.InputStream.html)
|
||||
/// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
||||
/// to given [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html).
|
||||
///
|
||||
/// Useful to create dynamically allocated, memory-safe buffer
|
||||
/// from remote connections, where final size of target data could not be known by Gemini protocol restrictions.
|
||||
/// Also, could be useful for [Pixbuf](https://docs.gtk.org/gdk-pixbuf/class.Pixbuf.html) or
|
||||
/// loading widgets like [Spinner](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Spinner.html)
|
||||
/// to display bytes on async data loading.
|
||||
///
|
||||
/// * this function takes entire `SocketConnection` reference (not `MemoryInputStream`) just to keep connection alive in the async context
|
||||
///
|
||||
/// **Implementation**
|
||||
///
|
||||
/// * recursively read all bytes from `InputStream` for `SocketConnection` according to `bytes_in_chunk` argument
|
||||
/// * calculates total bytes length on every chunk iteration, validate sum with `bytes_total_limit` argument
|
||||
/// * stop reading `InputStream` with `Result` on zero bytes in chunk received, otherwise continue next chunk request in loop
|
||||
/// * applies callback functions:
|
||||
/// * `on_chunk` - return reference to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) and `bytes_total` collected for every chunk in reading loop
|
||||
/// * `on_complete` - return `MemoryInputStream` on success or `Error` on failure as `Result`
|
||||
pub fn read_all_from_socket_connection_async(
|
||||
/// 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,
|
||||
socket_connection: SocketConnection,
|
||||
cancelable: Option<Cancellable>,
|
||||
io_stream: impl IsA<IOStream>,
|
||||
priority: Priority,
|
||||
bytes_in_chunk: usize,
|
||||
bytes_total_limit: usize,
|
||||
bytes_total: usize,
|
||||
on_chunk: impl Fn((Bytes, usize)) + 'static,
|
||||
on_complete: impl FnOnce(Result<MemoryInputStream, (Error, Option<&str>)>) + 'static,
|
||||
cancellable: Cancellable,
|
||||
mut size: Size,
|
||||
(on_chunk, on_complete): (
|
||||
impl Fn(usize, usize) + 'static,
|
||||
impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static,
|
||||
),
|
||||
) {
|
||||
socket_connection.input_stream().read_bytes_async(
|
||||
bytes_in_chunk,
|
||||
io_stream.input_stream().read_bytes_async(
|
||||
size.chunk,
|
||||
priority,
|
||||
cancelable.clone().as_ref(),
|
||||
Some(&cancellable.clone()),
|
||||
move |result| match result {
|
||||
Ok(bytes) => {
|
||||
// Update bytes total
|
||||
let bytes_total = bytes_total + bytes.len();
|
||||
let len = bytes.len(); // calculate once
|
||||
|
||||
// Callback chunk function
|
||||
on_chunk((bytes.clone(), bytes_total));
|
||||
|
||||
// Validate max size
|
||||
if bytes_total > bytes_total_limit {
|
||||
return on_complete(Err((Error::BytesTotal, None)));
|
||||
// is end of stream
|
||||
if len == 0 {
|
||||
return on_complete(Ok((memory_input_stream, size.total)));
|
||||
}
|
||||
|
||||
// No bytes were read, end of stream
|
||||
if bytes.len() == 0 {
|
||||
return on_complete(Ok(memory_input_stream));
|
||||
}
|
||||
// callback chunk function
|
||||
size.total += len;
|
||||
on_chunk(len, size.total);
|
||||
|
||||
// Write chunk bytes
|
||||
// push bytes into the memory pool
|
||||
memory_input_stream.add_bytes(&bytes);
|
||||
|
||||
// Continue
|
||||
read_all_from_socket_connection_async(
|
||||
// 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,
|
||||
socket_connection,
|
||||
cancelable,
|
||||
io_stream,
|
||||
priority,
|
||||
bytes_in_chunk,
|
||||
bytes_total_limit,
|
||||
bytes_total,
|
||||
on_chunk,
|
||||
on_complete,
|
||||
);
|
||||
}
|
||||
Err(reason) => {
|
||||
on_complete(Err((Error::InputStream, Some(reason.message()))));
|
||||
cancellable,
|
||||
size,
|
||||
(on_chunk, on_complete),
|
||||
)
|
||||
}
|
||||
Err(e) => on_complete(Err(Error::InputStream(memory_input_stream, e))),
|
||||
},
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
BytesTotal,
|
||||
InputStream,
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
src/gio/memory_input_stream/size.rs
Normal file
16
src/gio/memory_input_stream/size.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/gio/network_address.rs
Normal file
24
src/gio/network_address.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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
|
||||
},
|
||||
))
|
||||
}
|
||||
16
src/gio/network_address/error.rs
Normal file
16
src/gio/network_address/error.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/lib.rs
16
src/lib.rs
|
|
@ -1,2 +1,18 @@
|
|||
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");
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
// @TODO
|
||||
Loading…
Add table
Add a link
Reference in a new issue