Compare commits

..

53 commits
0.17.2 ... main

Author SHA1 Message Date
yggverse
11d17e004e update version 2025-11-07 21:15:20 +02:00
yggverse
bba51e38e8 apply fmt updates 2025-10-19 22:46:49 +03:00
yggverse
0f6eaa563c update version 2025-10-19 22:38:51 +03:00
yggverse
7e9ecf64b3 implement default trait 2025-10-19 22:37:48 +03:00
yggverse
f8537e4ab6 use latest dependencies version 2025-07-23 05:08:13 +03:00
yggverse
5019e66667 use latest 0.20 api 2025-07-23 04:29:01 +03:00
yggverse
d8e0a8e35a update dependencies version 2025-07-23 03:07:24 +03:00
yggverse
c5d10e020a return Connection on Request error 2025-07-23 01:27:20 +03:00
yggverse
e878fe4ba2 return NetworkAddress on Error::Connect 2025-07-22 09:49:39 +03:00
yggverse
cc1018224a reorganize error types, return socket_connection on init error 2025-07-22 09:46:32 +03:00
yggverse
44196608ce implement optional TOFU validation 2025-07-22 08:48:56 +03:00
yggverse
bb5b1dfb53 apply clippy optimizations 2025-07-22 08:44:50 +03:00
yggverse
c79f386bf1 update version 2025-03-28 00:33:33 +02:00
yggverse
9bbaecf344 make Size struct for tuple argument, move Size to separated mod 2025-03-27 21:33:50 +02:00
yggverse
4dddbd5f8a make Size struct for tuple argument 2025-03-27 21:14:14 +02:00
yggverse
b6ea830545 update example 2025-03-25 07:38:54 +02:00
yggverse
8ee088270f implement high-level getters, add comments, improve tests 2025-03-25 07:36:48 +02:00
yggverse
46da3a031a remove extras 2025-03-25 07:08:20 +02:00
yggverse
ea1fb8ea66 update temporary status codes api 2025-03-25 05:13:56 +02:00
yggverse
c9a59e76ee optimize tests format 2025-03-25 05:13:40 +02:00
yggverse
e96ff688b3 update permanent status codes api 2025-03-25 04:43:56 +02:00
yggverse
0c75da793f add missed tests members, enshort local var names 2025-03-25 03:26:39 +02:00
yggverse
064c4107f3 reduce local var names len 2025-03-25 03:21:59 +02:00
yggverse
d565d56c17 implement message_or_default method, add comments, add missed members test 2025-03-25 03:20:40 +02:00
yggverse
3b24625d66 implement message_or_default method, add comments 2025-03-25 03:01:19 +02:00
yggverse
f513747e86 add alias getters test 2025-03-25 02:21:03 +02:00
yggverse
5229cdae85 reorganize redirection structs format: make constructors lazy, parse members on get 2025-03-25 02:18:02 +02:00
yggverse
473ed48715 make final codes public, add comments 2025-03-24 23:32:18 +02:00
yggverse
4eb998ef20 draft potential test 2025-03-24 22:51:55 +02:00
yggverse
232531a0bc reorganize certificate structs format: make constructors lazy, parse members on get 2025-03-24 22:50:03 +02:00
yggverse
1b96270598 remove deprecated enum values 2025-03-24 22:38:07 +02:00
yggverse
845f3dc77e enshort var names 2025-03-24 22:36:00 +02:00
yggverse
e94923ecb5 fix last byte inclusion 2025-03-24 20:49:33 +02:00
yggverse
a32eccf5cb reorganize input format: make constructors lazy, parse members on get 2025-03-24 20:46:54 +02:00
yggverse
161142c809 rename mode const 2025-03-24 19:57:54 +02:00
yggverse
8feab6b93b rename constructors 2025-03-24 19:40:12 +02:00
yggverse
5360c6bf19 close code members 2025-03-24 18:31:55 +02:00
yggverse
68277f8e83 update example 2025-03-24 07:43:44 +02:00
yggverse
71043bbf73 remove extra format 2025-03-24 07:38:26 +02:00
yggverse
3de096cced add tests 2025-03-24 07:35:57 +02:00
yggverse
68e7894125 apply clippy 2025-03-24 07:10:22 +02:00
yggverse
0717e473b7 remove unsupported modes, add comments 2025-03-24 07:06:54 +02:00
yggverse
7c518cecf6 begin header holder implementation with lazy parser by getters, add request::Mode, add common header_bytes helper 2025-03-24 06:50:08 +02:00
yggverse
a12a73d311 hold NetworkAddress and SocketConnection as the Connection members 2025-03-22 19:03:42 +02:00
yggverse
2102d8887a fix codes 2025-03-19 15:07:22 +02:00
yggverse
9eb21bb6a3 Revert "hold raw header string"
This reverts commit 5bb52fbd8c.
2025-03-19 15:06:53 +02:00
yggverse
3f968d87b1 update error enum 2025-03-19 03:25:55 +02:00
yggverse
ab8eb402a8 decode header bytes only 2025-03-19 03:23:20 +02:00
yggverse
6dbf49cea3 validate header len 2025-03-19 03:13:37 +02:00
yggverse
b62f990bf2 fix codes, validate header len 2025-03-19 03:12:43 +02:00
yggverse
376473660f update minor version 2025-03-19 01:17:14 +02:00
yggverse
5bb52fbd8c hold raw header string 2025-03-19 01:16:51 +02:00
yggverse
af8a972cca update version 2025-03-18 00:48:58 +02:00
65 changed files with 2910 additions and 915 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "ggemini"
version = "0.17.2"
version = "0.20.1"
edition = "2024"
license = "MIT"
readme = "README.md"
@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini"
[dependencies.gio]
package = "gio"
version = "0.20.9"
version = "0.21.0"
features = ["v2_70"]
[dependencies.glib]
package = "glib"
version = "0.20.9"
version = "0.21.0"
features = ["v2_66"]

View file

@ -43,7 +43,7 @@ use gio::*;
use glib::*;
use ggemini::client::{
connection::{Request, Response},
connection::{request::{Mode, Request}, Response},
Client,
};
@ -51,13 +51,15 @@ 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 `GTlsCertificate`
None, // optional auth `GTlsCertificate`
None, // optional TOFU `GTlsCertificate` array
|result| match result {
Ok((response, _connection)) => match response {
Response::Success(success) => match success.mime() {
Response::Success(success) => match success.mime().unwrap().as_str() {
"text/gemini" => todo!(),
_ => todo!(),
},

View file

@ -59,7 +59,8 @@ impl Client {
request: Request,
priority: Priority,
cancellable: Cancellable,
certificate: Option<TlsCertificate>,
client_certificate: Option<TlsCertificate>,
server_certificates: Option<Vec<TlsCertificate>>,
callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static,
) {
// Begin new connection
@ -73,30 +74,33 @@ impl Client {
move |result| match result {
Ok(socket_connection) => {
match Connection::build(
socket_connection,
certificate,
Some(network_address),
socket_connection.clone(),
network_address,
client_certificate,
server_certificates,
is_session_resumption,
) {
Ok(connection) => connection.request_async(
Ok(connection) => connection.clone().request_async(
request,
priority,
cancellable,
move |result| {
callback(match result {
Ok(response) => Ok(response),
Err(e) => Err(Error::Connection(e)),
Err(e) => Err(Error::Request(connection, e)),
})
},
),
Err(e) => callback(Err(Error::Connection(e))),
Err(e) => {
callback(Err(Error::Connection(socket_connection, e)))
}
}
}
Err(e) => callback(Err(Error::Connect(e))),
Err(e) => callback(Err(Error::Connect(network_address, e))),
}
})
}
Err(e) => callback(Err(Error::Request(e))),
Err(e) => callback(Err(Error::NetworkAddress(e))),
}
}

View file

@ -3,21 +3,22 @@ pub mod request;
pub mod response;
pub use error::Error;
pub use request::Request;
pub use request::{Mode, Request};
pub use response::Response;
// Local dependencies
use gio::{
Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection,
prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt},
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,
}
@ -27,24 +28,28 @@ impl Connection {
/// Create new `Self`
pub fn build(
socket_connection: SocketConnection,
certificate: Option<TlsCertificate>,
server_identity: Option<NetworkAddress>,
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,
server_identity.as_ref(),
Some(&network_address),
server_certificates,
is_session_resumption,
) {
Ok(tls_client_connection) => {
if let Some(ref certificate) = certificate {
tls_client_connection.set_certificate(certificate);
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,
})
}
@ -70,36 +75,40 @@ impl Connection {
Some(&cancellable.clone()),
move |result| match result {
Ok(_) => match request {
Request::Gemini { .. } => Response::from_connection_async(
self,
priority,
cancellable,
|result, connection| {
callback(match result {
Ok(response) => Ok((response, connection)),
Err(e) => Err(Error::Response(e)),
})
},
),
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, .. } => output_stream.write_all_async(
Request::Titan { data, mode, .. } => output_stream.write_all_async(
data,
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok(_) => Response::from_connection_async(
self,
priority,
cancellable,
|result, connection| {
callback(match result {
Ok(response) => Ok((response, connection)),
Err(e) => Err(Error::Response(e)),
})
},
),
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))),
},
),
@ -120,14 +129,15 @@ impl Connection {
}
}
// Helpers
// 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)
pub fn new_tls_client_connection(
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) {
@ -141,9 +151,19 @@ pub fn new_tls_client_connection(
// https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections
tls_client_connection.set_require_close_notify(true);
// @TODO validate
// https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation
tls_client_connection.connect_accept_certificate(|_, _, _| 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)
}

View file

@ -1,5 +1,8 @@
pub mod error;
pub mod mode;
pub use error::Error;
pub use mode::Mode;
// Local dependencies
@ -10,6 +13,7 @@ use glib::{Bytes, Uri, UriHideFlags};
pub enum Request {
Gemini {
uri: Uri,
mode: Mode,
},
Titan {
uri: Uri,
@ -18,6 +22,7 @@ pub enum Request {
/// but server MAY reject the request without `mime` value provided.
mime: Option<String>,
token: Option<String>,
mode: Mode,
},
}
@ -27,12 +32,13 @@ impl Request {
/// Generate header string for `Self`
pub fn header(&self) -> String {
match self {
Self::Gemini { uri } => format!("{uri}\r\n"),
Self::Gemini { uri, .. } => format!("{uri}\r\n"),
Self::Titan {
uri,
data,
mime,
token,
..
} => {
let mut header = format!(
"{};size={}",
@ -57,7 +63,7 @@ impl Request {
/// 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::Gemini { uri, .. } => uri,
Self::Titan { uri, .. } => uri,
}
}
@ -79,7 +85,8 @@ fn test_gemini_header() {
assert_eq!(
Request::Gemini {
uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap()
uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(),
mode: Mode::HeaderOnly
}
.header(),
format!("{REQUEST}\r\n")
@ -103,7 +110,8 @@ fn test_titan_header() {
.unwrap(),
data: Bytes::from(DATA),
mime: Some(MIME.to_string()),
token: Some(TOKEN.to_string())
token: Some(TOKEN.to_string()),
mode: Mode::HeaderOnly
}
.header(),
format!(

View 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,
}

View file

@ -6,7 +6,7 @@ pub mod redirect;
pub mod success;
pub use certificate::Certificate;
pub use error::Error;
pub use error::{Error, HeaderBytesError};
pub use failure::Failure;
pub use input::Input;
pub use redirect::Redirect;
@ -29,13 +29,13 @@ pub enum Response {
impl Response {
/// Asynchronously create new `Self` for given `Connection`
pub fn from_connection_async(
pub fn header_from_connection_async(
connection: Connection,
priority: Priority,
cancellable: Cancellable,
callback: impl FnOnce(Result<Self, Error>, Connection) + 'static,
) {
from_stream_async(
header_from_stream_async(
Vec::with_capacity(HEADER_LEN),
connection.stream(),
cancellable,
@ -44,7 +44,7 @@ impl Response {
callback(
match result {
Ok(buffer) => match buffer.first() {
Some(byte) => match byte {
Some(b) => match b {
b'1' => match Input::from_utf8(&buffer) {
Ok(input) => Ok(Self::Input(input)),
Err(e) => Err(Error::Input(e)),
@ -65,16 +65,16 @@ impl Response {
Ok(certificate) => Ok(Self::Certificate(certificate)),
Err(e) => Err(Error::Certificate(e)),
},
_ => Err(Error::Code),
b => Err(Error::Code(*b)),
},
None => Err(Error::Protocol),
None => Err(Error::Protocol(buffer)),
},
Err(e) => Err(e),
},
connection,
)
},
);
)
}
}
@ -84,43 +84,63 @@ impl Response {
///
/// Return UTF-8 buffer collected
/// * requires `IOStream` reference to keep `Connection` active in async thread
fn from_stream_async(
fn header_from_stream_async(
mut buffer: Vec<u8>,
stream: impl IsA<IOStream>,
cancellable: Cancellable,
priority: Priority,
on_complete: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
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((mut bytes, size)) => {
// Expect valid header length
if size == 0 || buffer.len() >= HEADER_LEN {
return on_complete(Err(Error::Protocol));
Ok((bytes, size)) => {
if size == 0 {
return callback(Ok(buffer));
}
// Read next byte without record
if bytes.contains(&b'\r') {
return from_stream_async(buffer, stream, cancellable, priority, on_complete);
if buffer.len() + bytes.len() > HEADER_LEN {
buffer.extend(bytes);
return callback(Err(Error::Protocol(buffer)));
}
// Complete without record
if bytes.contains(&b'\n') {
return on_complete(Ok(buffer));
if bytes[0] == b'\r' {
buffer.extend(bytes);
return header_from_stream_async(
buffer,
stream,
cancellable,
priority,
callback,
);
}
// Record
buffer.append(&mut bytes);
// Continue
from_stream_async(buffer, stream, cancellable, priority, on_complete);
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)) => on_complete(Err(Error::Stream(e, data))),
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)
}

View file

@ -1,19 +1,24 @@
pub mod error;
pub use error::Error;
pub mod not_authorized;
pub mod not_valid;
pub mod required;
const REQUIRED: (u8, &str) = (10, "Certificate required");
const NOT_AUTHORIZED: (u8, &str) = (11, "Certificate not authorized");
const NOT_VALID: (u8, &str) = (11, "Certificate not valid");
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 { message: Option<String> },
Required(Required),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
NotAuthorized { message: Option<String> },
NotAuthorized(NotAuthorized),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
NotValid { message: Option<String> },
NotValid(NotValid),
}
impl Certificate {
@ -21,95 +26,86 @@ impl Certificate {
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
use std::str::FromStr;
match std::str::from_utf8(buffer) {
Ok(header) => Self::from_str(header),
Err(e) => Err(Error::Utf8Error(e)),
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
pub fn to_code(&self) -> u8 {
match self {
Self::Required { .. } => REQUIRED,
Self::NotAuthorized { .. } => NOT_AUTHORIZED,
Self::NotValid { .. } => NOT_VALID,
}
.0
}
/// 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 { message } => message,
Self::NotAuthorized { message } => message,
Self::NotValid { message } => message,
Self::Required(required) => required.message(),
Self::NotAuthorized(not_authorized) => not_authorized.message(),
Self::NotValid(not_valid) => not_valid.message(),
}
.as_deref()
}
}
impl std::fmt::Display for Certificate {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Required { .. } => REQUIRED,
Self::NotAuthorized { .. } => NOT_AUTHORIZED,
Self::NotValid { .. } => NOT_VALID,
}
.1
)
/// 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(),
}
}
}
impl std::str::FromStr for Certificate {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
if let Some(postfix) = header.strip_prefix("60") {
return Ok(Self::Required {
message: message(postfix),
});
/// 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(),
}
if let Some(postfix) = header.strip_prefix("61") {
return Ok(Self::NotAuthorized {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("62") {
return Ok(Self::NotValid {
message: message(postfix),
});
}
Err(Error::Code)
}
}
// Tools
fn message(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() {
None
} else {
Some(value.to_string())
/// 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_from_str() {
use std::str::FromStr;
let required = Certificate::from_str("60 Message\r\n").unwrap();
assert_eq!(required.message(), Some("Message"));
assert_eq!(required.to_code(), REQUIRED.0);
assert_eq!(required.to_string(), REQUIRED.1);
let required = Certificate::from_str("60\r\n").unwrap();
assert_eq!(required.message(), None);
assert_eq!(required.to_code(), REQUIRED.0);
assert_eq!(required.to_string(), REQUIRED.1);
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);
}

View file

@ -1,22 +1,39 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Utf8Error(Utf8Error),
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::Code => {
write!(f, "Status code error")
Self::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 error: {e}")
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")
}
}
}

View 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());
}

View 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}")
}
}
}
}

View 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());
}

View 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}")
}
}
}
}

View 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());
}

View 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}")
}
}
}
}

View file

@ -6,10 +6,10 @@ use std::{
#[derive(Debug)]
pub enum Error {
Certificate(super::certificate::Error),
Code,
Code(u8),
Failure(super::failure::Error),
Input(super::input::Error),
Protocol,
Protocol(Vec<u8>),
Redirect(super::redirect::Error),
Stream(glib::Error, Vec<u8>),
Success(super::success::Error),
@ -22,8 +22,8 @@ impl Display for Error {
Self::Certificate(e) => {
write!(f, "Certificate error: {e}")
}
Self::Code => {
write!(f, "Code group error")
Self::Code(b) => {
write!(f, "Unexpected status code byte: {b}")
}
Self::Failure(e) => {
write!(f, "Failure error: {e}")
@ -31,7 +31,7 @@ impl Display for Error {
Self::Input(e) => {
write!(f, "Input error: {e}")
}
Self::Protocol => {
Self::Protocol(..) => {
write!(f, "Protocol error")
}
Self::Redirect(e) => {
@ -49,3 +49,22 @@ impl Display for Error {
}
}
}
#[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")
}
}
}
}

View file

@ -21,7 +21,7 @@ impl Failure {
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.first() {
Some(byte) => match byte {
Some(b) => match b {
b'4' => match Temporary::from_utf8(buffer) {
Ok(input) => Ok(Self::Temporary(input)),
Err(e) => Err(Error::Temporary(e)),
@ -30,7 +30,7 @@ impl Failure {
Ok(failure) => Ok(Self::Permanent(failure)),
Err(e) => Err(Error::Permanent(e)),
},
_ => Err(Error::Code),
b => Err(Error::Code(*b)),
},
None => Err(Error::Protocol),
}
@ -38,17 +38,52 @@ impl Failure {
// Getters
pub fn to_code(&self) -> u8 {
match self {
Self::Permanent(permanent) => permanent.to_code(),
Self::Temporary(temporary) => temporary.to_code(),
}
}
/// 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);
}
}

View file

@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Code(u8),
Permanent(super::permanent::Error),
Protocol,
Temporary(super::temporary::Error),
@ -11,8 +11,8 @@ pub enum Error {
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Code group error")
Self::Code(b) => {
write!(f, "Unexpected status code byte: {b}")
}
Self::Permanent(e) => {
write!(f, "Permanent failure group error: {e}")

View file

@ -1,24 +1,31 @@
pub mod bad_request;
pub mod default;
pub mod error;
pub use error::Error;
pub mod gone;
pub mod not_found;
pub mod proxy_request_refused;
const DEFAULT: (u8, &str) = (50, "Unspecified");
const NOT_FOUND: (u8, &str) = (51, "Not found");
const GONE: (u8, &str) = (52, "Gone");
const PROXY_REQUEST_REFUSED: (u8, &str) = (53, "Proxy request refused");
const BAD_REQUEST: (u8, &str) = (59, "bad-request");
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 { message: Option<String> },
Default(Default),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found
NotFound { message: Option<String> },
NotFound(NotFound),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone
Gone { message: Option<String> },
Gone(Gone),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused
ProxyRequestRefused { message: Option<String> },
ProxyRequestRefused(ProxyRequestRefused),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request
BadRequest { message: Option<String> },
BadRequest(BadRequest),
}
impl Permanent {
@ -26,154 +33,94 @@ impl Permanent {
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
use std::str::FromStr;
match std::str::from_utf8(buffer) {
Ok(header) => Self::from_str(header),
Err(e) => Err(Error::Utf8Error(e)),
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 to_code(&self) -> u8 {
match self {
Self::Default { .. } => DEFAULT,
Self::NotFound { .. } => NOT_FOUND,
Self::Gone { .. } => GONE,
Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED,
Self::BadRequest { .. } => BAD_REQUEST,
}
.0
}
pub fn message(&self) -> Option<&str> {
match self {
Self::Default { message } => message,
Self::NotFound { message } => message,
Self::Gone { message } => message,
Self::ProxyRequestRefused { message } => message,
Self::BadRequest { message } => message,
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(),
}
.as_deref()
}
}
impl std::fmt::Display for Permanent {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Default { .. } => DEFAULT,
Self::NotFound { .. } => NOT_FOUND,
Self::Gone { .. } => GONE,
Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED,
Self::BadRequest { .. } => BAD_REQUEST,
/// 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()
}
.1
)
Self::BadRequest(bad_request) => bad_request.message_or_default(),
}
}
}
impl std::str::FromStr for Permanent {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
if let Some(postfix) = header.strip_prefix("50") {
return Ok(Self::Default {
message: message(postfix),
});
/// 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(),
}
if let Some(postfix) = header.strip_prefix("51") {
return Ok(Self::NotFound {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("52") {
return Ok(Self::Gone {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("53") {
return Ok(Self::ProxyRequestRefused {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("59") {
return Ok(Self::BadRequest {
message: message(postfix),
});
}
Err(Error::Code)
}
}
// Tools
fn message(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() {
None
} else {
Some(value.to_string())
/// 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_from_str() {
use std::str::FromStr;
// 50
let default = Permanent::from_str("50 Message\r\n").unwrap();
assert_eq!(default.message(), Some("Message"));
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
let default = Permanent::from_str("50\r\n").unwrap();
assert_eq!(default.message(), None);
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
// 51
let not_found = Permanent::from_str("51 Message\r\n").unwrap();
assert_eq!(not_found.message(), Some("Message"));
assert_eq!(not_found.to_code(), NOT_FOUND.0);
assert_eq!(not_found.to_string(), NOT_FOUND.1);
let not_found = Permanent::from_str("51\r\n").unwrap();
assert_eq!(not_found.message(), None);
assert_eq!(not_found.to_code(), NOT_FOUND.0);
assert_eq!(not_found.to_string(), NOT_FOUND.1);
// 52
let gone = Permanent::from_str("52 Message\r\n").unwrap();
assert_eq!(gone.message(), Some("Message"));
assert_eq!(gone.to_code(), GONE.0);
assert_eq!(gone.to_string(), GONE.1);
let gone = Permanent::from_str("52\r\n").unwrap();
assert_eq!(gone.message(), None);
assert_eq!(gone.to_code(), GONE.0);
assert_eq!(gone.to_string(), GONE.1);
// 53
let proxy_request_refused = Permanent::from_str("53 Message\r\n").unwrap();
assert_eq!(proxy_request_refused.message(), Some("Message"));
assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0);
assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1);
let proxy_request_refused = Permanent::from_str("53\r\n").unwrap();
assert_eq!(proxy_request_refused.message(), None);
assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0);
assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1);
// 59
let bad_request = Permanent::from_str("59 Message\r\n").unwrap();
assert_eq!(bad_request.message(), Some("Message"));
assert_eq!(bad_request.to_code(), BAD_REQUEST.0);
assert_eq!(bad_request.to_string(), BAD_REQUEST.1);
let bad_request = Permanent::from_str("59\r\n").unwrap();
assert_eq!(bad_request.message(), None);
assert_eq!(bad_request.to_code(), BAD_REQUEST.0);
assert_eq!(bad_request.to_string(), BAD_REQUEST.1);
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);
}
}

View file

@ -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());
}

View 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}")
}
}
}
}

View 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());
}

View 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}")
}
}
}
}

View file

@ -1,22 +1,47 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Utf8Error(Utf8Error),
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::Code => {
write!(f, "Status code error")
Self::BadRequest(e) => {
write!(f, "BadRequest parse error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 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")
}
}
}

View 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());
}

View 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}")
}
}
}
}

View file

@ -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());
}

View 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}")
}
}
}
}

View file

@ -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());
}

View 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}")
}
}
}
}

View file

@ -1,24 +1,31 @@
pub mod cgi_error;
pub mod default;
pub mod error;
pub use error::Error;
pub mod proxy_error;
pub mod server_unavailable;
pub mod slow_down;
const DEFAULT: (u8, &str) = (40, "Unspecified");
const SERVER_UNAVAILABLE: (u8, &str) = (41, "Server unavailable");
const CGI_ERROR: (u8, &str) = (42, "CGI error");
const PROXY_ERROR: (u8, &str) = (43, "Proxy error");
const SLOW_DOWN: (u8, &str) = (44, "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 { message: Option<String> },
Default(Default),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable
ServerUnavailable { message: Option<String> },
ServerUnavailable(ServerUnavailable),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error
CgiError { message: Option<String> },
CgiError(CgiError),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error
ProxyError { message: Option<String> },
ProxyError(ProxyError),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down
SlowDown { message: Option<String> },
SlowDown(SlowDown),
}
impl Temporary {
@ -26,154 +33,94 @@ impl Temporary {
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
use std::str::FromStr;
match std::str::from_utf8(buffer) {
Ok(header) => Self::from_str(header),
Err(e) => Err(Error::Utf8Error(e)),
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 to_code(&self) -> u8 {
match self {
Self::Default { .. } => DEFAULT,
Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE,
Self::CgiError { .. } => CGI_ERROR,
Self::ProxyError { .. } => PROXY_ERROR,
Self::SlowDown { .. } => SLOW_DOWN,
}
.0
}
pub fn message(&self) -> Option<&str> {
match self {
Self::Default { message } => message,
Self::ServerUnavailable { message } => message,
Self::CgiError { message } => message,
Self::ProxyError { message } => message,
Self::SlowDown { message } => message,
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(),
}
.as_deref()
}
}
impl std::fmt::Display for Temporary {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Default { .. } => DEFAULT,
Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE,
Self::CgiError { .. } => CGI_ERROR,
Self::ProxyError { .. } => PROXY_ERROR,
Self::SlowDown { .. } => SLOW_DOWN,
}
.1
)
/// 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(),
}
}
}
impl std::str::FromStr for Temporary {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
if let Some(postfix) = header.strip_prefix("40") {
return Ok(Self::Default {
message: message(postfix),
});
/// 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(),
}
if let Some(postfix) = header.strip_prefix("41") {
return Ok(Self::ServerUnavailable {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("42") {
return Ok(Self::CgiError {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("43") {
return Ok(Self::ProxyError {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("44") {
return Ok(Self::SlowDown {
message: message(postfix),
});
}
Err(Error::Code)
}
}
// Tools
fn message(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() {
None
} else {
Some(value.to_string())
/// 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_from_str() {
use std::str::FromStr;
// 40
let default = Temporary::from_str("40 Message\r\n").unwrap();
assert_eq!(default.message(), Some("Message"));
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
let default = Temporary::from_str("40\r\n").unwrap();
assert_eq!(default.message(), None);
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
// 41
let server_unavailable = Temporary::from_str("41 Message\r\n").unwrap();
assert_eq!(server_unavailable.message(), Some("Message"));
assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0);
assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1);
let server_unavailable = Temporary::from_str("41\r\n").unwrap();
assert_eq!(server_unavailable.message(), None);
assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0);
assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1);
// 42
let cgi_error = Temporary::from_str("42 Message\r\n").unwrap();
assert_eq!(cgi_error.message(), Some("Message"));
assert_eq!(cgi_error.to_code(), CGI_ERROR.0);
assert_eq!(cgi_error.to_string(), CGI_ERROR.1);
let cgi_error = Temporary::from_str("42\r\n").unwrap();
assert_eq!(cgi_error.message(), None);
assert_eq!(cgi_error.to_code(), CGI_ERROR.0);
assert_eq!(cgi_error.to_string(), CGI_ERROR.1);
// 43
let proxy_error = Temporary::from_str("43 Message\r\n").unwrap();
assert_eq!(proxy_error.message(), Some("Message"));
assert_eq!(proxy_error.to_code(), PROXY_ERROR.0);
assert_eq!(proxy_error.to_string(), PROXY_ERROR.1);
let proxy_error = Temporary::from_str("43\r\n").unwrap();
assert_eq!(proxy_error.message(), None);
assert_eq!(proxy_error.to_code(), PROXY_ERROR.0);
assert_eq!(proxy_error.to_string(), PROXY_ERROR.1);
// 44
let slow_down = Temporary::from_str("44 Message\r\n").unwrap();
assert_eq!(slow_down.message(), Some("Message"));
assert_eq!(slow_down.to_code(), SLOW_DOWN.0);
assert_eq!(slow_down.to_string(), SLOW_DOWN.1);
let slow_down = Temporary::from_str("44\r\n").unwrap();
assert_eq!(slow_down.message(), None);
assert_eq!(slow_down.to_code(), SLOW_DOWN.0);
assert_eq!(slow_down.to_string(), SLOW_DOWN.1);
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);
}
}

View file

@ -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());
}

View 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}")
}
}
}
}

View 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());
}

View 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}")
}
}
}
}

View file

@ -1,22 +1,47 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Utf8Error(Utf8Error),
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::Code => {
write!(f, "Status code error")
Self::CgiError(e) => {
write!(f, "CgiError parse error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 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")
}
}
}

View file

@ -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());
}

View 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}")
}
}
}
}

View file

@ -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());
}

View 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}")
}
}
}
}

View file

@ -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());
}

View 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}")
}
}
}
}

View file

@ -1,12 +1,17 @@
pub mod default;
pub mod error;
pub mod sensitive;
pub use default::Default;
pub use error::Error;
pub use sensitive::Sensitive;
const DEFAULT: (u8, &str) = (10, "Input");
const SENSITIVE: (u8, &str) = (11, "Sensitive input");
const CODE: u8 = b'1';
/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected)
pub enum Input {
Default { message: Option<String> },
Sensitive { message: Option<String> },
Default(Default),
Sensitive(Sensitive),
}
impl Input {
@ -14,97 +19,74 @@ impl Input {
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
use std::str::FromStr;
match std::str::from_utf8(buffer) {
Ok(header) => Self::from_str(header),
Err(e) => Err(Error::Utf8Error(e)),
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 to_code(&self) -> u8 {
match self {
Self::Default { .. } => DEFAULT,
Self::Sensitive { .. } => SENSITIVE,
}
.0
}
pub fn message(&self) -> Option<&str> {
match self {
Self::Default { message } => message,
Self::Sensitive { message } => message,
Self::Default(default) => default.message(),
Self::Sensitive(sensitive) => sensitive.message(),
}
.as_deref()
}
}
impl std::fmt::Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Default { .. } => DEFAULT,
Self::Sensitive { .. } => SENSITIVE,
}
.1
)
}
}
impl std::str::FromStr for Input {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
if let Some(postfix) = header.strip_prefix("10") {
return Ok(Self::Default {
message: message(postfix),
});
/// 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(),
}
if let Some(postfix) = header.strip_prefix("11") {
return Ok(Self::Sensitive {
message: message(postfix),
});
}
Err(Error::Protocol)
}
}
// Tools
/// 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(),
}
}
fn message(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() {
None
} else {
Some(value.to_string())
/// 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_from_str() {
use std::str::FromStr;
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
let default = Input::from_str("10 Default\r\n").unwrap();
assert_eq!(default.message(), Some("Default"));
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
let default = Input::from_str("10\r\n").unwrap();
assert_eq!(default.message(), None);
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
t("10 Default\r\n", Some("Default"));
t("10\r\n", None);
// 11
let sensitive = Input::from_str("11 Sensitive\r\n").unwrap();
assert_eq!(sensitive.message(), Some("Sensitive"));
assert_eq!(sensitive.to_code(), SENSITIVE.0);
assert_eq!(sensitive.to_string(), SENSITIVE.1);
let sensitive = Input::from_str("11\r\n").unwrap();
assert_eq!(sensitive.message(), None);
assert_eq!(sensitive.to_code(), SENSITIVE.0);
assert_eq!(sensitive.to_string(), SENSITIVE.1);
t("11 Sensitive\r\n", Some("Sensitive"));
t("11\r\n", None);
}

View 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());
}

View 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}")
}
}
}
}

View file

@ -1,22 +1,35 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Protocol,
Utf8Error(Utf8Error),
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::Utf8Error(e) => {
write!(f, "UTF-8 error: {e}")
Self::Default(e) => {
write!(f, "Default parse error: {e}")
}
Self::Protocol => {
write!(f, "Protocol error")
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")
}
}
}

View 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());
}

View 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}")
}
}
}
}

View file

@ -1,16 +1,23 @@
pub mod error;
pub use error::Error;
pub mod permanent;
pub mod temporary;
use glib::{GStringPtr, Uri, UriFlags};
pub use error::{Error, UriError};
pub use permanent::Permanent;
pub use temporary::Temporary;
const TEMPORARY: (u8, &str) = (30, "Temporary redirect");
const PERMANENT: (u8, &str) = (31, "Permanent redirect");
// 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 { target: String },
Temporary(Temporary),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
Permanent { target: String },
Permanent(Permanent),
}
impl Redirect {
@ -18,210 +25,161 @@ impl Redirect {
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
use std::str::FromStr;
match std::str::from_utf8(buffer) {
Ok(header) => Self::from_str(header),
Err(e) => Err(Error::Utf8Error(e)),
}
}
// Convertors
pub fn to_code(&self) -> u8 {
match self {
Self::Permanent { .. } => PERMANENT,
Self::Temporary { .. } => TEMPORARY,
}
.0
}
/// 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
pub fn to_uri(&self, base: &Uri) -> Result<Uri, Error> {
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 = self.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(Error::BaseHost),
}
} else {
postfix.to_string()
}
)
}
None => t.to_string(),
}
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)),
},
UriFlags::NONE,
) {
Ok(absolute) => Ok(absolute),
Err(e) => Err(Error::Uri(e)),
None => Err(Error::UndefinedFirstByte),
}
}
// Getters
pub fn target(&self) -> &str {
pub fn target(&self) -> Result<&str, Error> {
match self {
Self::Permanent { target } => target,
Self::Temporary { target } => target,
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),
}
}
}
impl std::fmt::Display for Redirect {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Permanent { .. } => PERMANENT,
Self::Temporary { .. } => TEMPORARY,
// 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(),
}
.1
)
}
}
impl std::str::FromStr for Redirect {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
use glib::{Regex, RegexCompileFlags, RegexMatchFlags};
let regex = Regex::split_simple(
r"^3(\d)\s([^\r\n]+)",
header,
RegexCompileFlags::DEFAULT,
RegexMatchFlags::DEFAULT,
);
match regex.get(1) {
Some(code) => match code.as_str() {
"0" => Ok(Self::Temporary {
target: target(regex.get(2))?,
}),
"1" => Ok(Self::Permanent {
target: target(regex.get(2))?,
}),
_ => todo!(),
},
None => Err(Error::Protocol),
}
}
}
fn target(value: Option<&GStringPtr>) -> Result<String, Error> {
match value {
Some(target) => {
let target = target.trim();
if target.is_empty() {
Err(Error::Target)
} else {
Ok(target.to_string())
}
}
None => Err(Error::Target),
},
UriFlags::NONE,
) {
Ok(absolute) => Ok(absolute),
Err(e) => Err(UriError::ParseRelative(e)),
}
}
#[test]
fn test() {
use std::str::FromStr;
{
let temporary = Redirect::from_str("30 /uri\r\n").unwrap();
assert_eq!(temporary.target(), "/uri");
assert_eq!(temporary.to_code(), TEMPORARY.0);
assert_eq!(temporary.to_string(), TEMPORARY.1);
let permanent = Redirect::from_str("31 /uri\r\n").unwrap();
assert_eq!(permanent.target(), "/uri");
assert_eq!(permanent.to_code(), PERMANENT.0);
assert_eq!(permanent.to_string(), PERMANENT.1);
}
{
let base = Uri::build(
UriFlags::NONE,
"gemini",
None,
Some("geminiprotocol.net"),
-1,
"/path/",
Some("query"),
Some("fragment"),
);
assert_eq!(
Redirect::from_str("30 /uri\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://geminiprotocol.net/uri"
);
assert_eq!(
Redirect::from_str("30 uri\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://geminiprotocol.net/path/uri"
);
assert_eq!(
Redirect::from_str("30 gemini://test.host/uri\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://test.host/uri"
);
assert_eq!(
Redirect::from_str("30 //\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://geminiprotocol.net/"
);
assert_eq!(
Redirect::from_str("30 //geminiprotocol.net/path\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://geminiprotocol.net/path"
);
assert_eq!(
Redirect::from_str("30 //:\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://geminiprotocol.net/"
);
/// 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/");
}

View file

@ -1,34 +1,55 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
BaseHost,
Uri(glib::Error),
Protocol,
Target,
Utf8Error(Utf8Error),
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::BaseHost => {
write!(f, "Base host required")
Self::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::Uri(e) => {
write!(f, "URI error: {e}")
Self::Permanent(e) => {
write!(f, "Permanent parse error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 error: {e}")
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::Protocol => {
write!(f, "Protocol error")
Self::Temporary(e) => {
write!(f, "Temporary parse error: {e}")
}
Self::Target => {
write!(f, "Target error")
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}")
}
}
}

View 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());
}

View 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}")
}
}
}
}

View 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())
}

View 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}")
}
}
}
}

View file

@ -1,89 +1,76 @@
pub mod default;
pub mod error;
pub use default::Default;
pub use error::Error;
const DEFAULT: (u8, &str) = (20, "Success");
const CODE: u8 = b'2';
pub enum Success {
Default { mime: String },
Default(Default),
// reserved for 2* codes
}
impl Success {
// Constructors
/// Create new `Self` from buffer include header bytes
/// Parse new `Self` from buffer bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
use std::str::FromStr;
match std::str::from_utf8(buffer) {
Ok(header) => Self::from_str(header),
Err(e) => Err(Error::Utf8Error(e)),
if buffer.first().is_none_or(|b| *b != CODE) {
return Err(Error::Code);
}
}
// Convertors
pub fn to_code(&self) -> u8 {
match self {
Self::Default { .. } => DEFAULT.0,
match Default::from_utf8(buffer) {
Ok(default) => Ok(Self::Default(default)),
Err(e) => Err(Error::Default(e)),
}
}
// Getters
pub fn mime(&self) -> &str {
/// Get header bytes for `Self` type
pub fn as_header_bytes(&self) -> &[u8] {
match self {
Self::Default { mime } => mime,
Self::Default(default) => default.header.as_bytes(),
}
}
}
impl std::fmt::Display for Success {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Default { .. } => DEFAULT.1,
}
)
/// Get header string for `Self` type
pub fn as_header_str(&self) -> &str {
match self {
Self::Default(default) => default.header.as_str(),
}
}
}
impl std::str::FromStr for Success {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
use glib::{Regex, RegexCompileFlags, RegexMatchFlags};
match Regex::split_simple(
r"^20\s([^\/]+\/[^\s;]+)",
header,
RegexCompileFlags::DEFAULT,
RegexMatchFlags::DEFAULT,
)
.get(1)
{
Some(mime) => {
let mime = mime.trim();
if mime.is_empty() {
Err(Error::Mime)
} else {
Ok(Self::Default {
mime: mime.to_lowercase(),
})
}
}
None => Err(Error::Protocol),
/// 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_from_str() {
use std::str::FromStr;
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();
let default = Success::from_str("20 text/gemini; charset=utf-8; lang=en\r\n").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_eq!(default.mime(), "text/gemini");
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
assert!(Success::from_utf8("40 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err())
}

View 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);
}

View 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}")
}
}
}
}

View 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());
}

View file

@ -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}")
}
}
}
}

View file

@ -1,26 +1,19 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Protocol,
Mime,
Utf8Error(Utf8Error),
Code,
Default(super::default::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Utf8Error(e) => {
write!(f, "UTF-8 error: {e}")
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Protocol => {
write!(f, "Protocol error")
}
Self::Mime => {
write!(f, "MIME error")
Self::Default(e) => {
write!(f, "Header error: {e}")
}
}
}

View file

@ -2,22 +2,29 @@ use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Connect(glib::Error),
Connection(crate::client::connection::Error),
Request(crate::client::connection::request::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::Connection(e) => {
write!(f, "Connection error: {e}")
}
Self::Connect(e) => {
Self::Connect(_, e) => {
write!(f, "Connect error: {e}")
}
Self::Request(e) => {
write!(f, "Request 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}")
}
}
}

View file

@ -1,5 +1,8 @@
pub mod error;
pub mod size;
pub use error::Error;
pub use size::Size;
use gio::{
Cancellable, FileOutputStream, IOStream,
@ -15,59 +18,50 @@ pub fn from_stream_async(
file_output_stream: FileOutputStream,
cancellable: Cancellable,
priority: Priority,
(chunk, limit, mut total): (
usize, // bytes_in_chunk
Option<usize>, // bytes_total_limit, `None` to unlimited
usize, // bytes_total
),
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(
chunk,
size.chunk,
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok(bytes) => {
total += bytes.len();
on_chunk(bytes.clone(), total);
size.total += bytes.len();
on_chunk(bytes.clone(), size.total);
if let Some(limit) = limit {
if total > limit {
return on_complete(Err(Error::BytesTotal(total, limit)));
}
if let Some(limit) = size.limit
&& size.total > limit
{
return on_complete(Err(Error::BytesTotal(size.total, limit)));
}
if bytes.len() == 0 {
return on_complete(Ok((file_output_stream, total)));
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.clone(),
bytes,
priority,
Some(&cancellable.clone()),
move |result| {
match result {
Ok(_) => {
// continue read..
from_stream_async(
io_stream,
file_output_stream,
cancellable,
priority,
(chunk, limit, total),
(on_chunk, on_complete),
);
}
Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))),
}
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))),
},

View 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,
}
}
}

View file

@ -1,5 +1,8 @@
pub mod error;
pub mod size;
pub use error::Error;
pub use size::Size;
use gio::{
Cancellable, IOStream, MemoryInputStream,
@ -17,7 +20,7 @@ pub fn from_stream_async(
io_stream: impl IsA<IOStream>,
priority: Priority,
cancelable: Cancellable,
(chunk, limit): (usize, usize),
size: Size,
(on_chunk, on_complete): (
impl Fn(usize, usize) + 'static,
impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static,
@ -28,7 +31,7 @@ pub fn from_stream_async(
io_stream,
priority,
cancelable,
(chunk, limit, 0),
size,
(on_chunk, on_complete),
);
}
@ -41,14 +44,14 @@ pub fn for_memory_input_stream_async(
io_stream: impl IsA<IOStream>,
priority: Priority,
cancellable: Cancellable,
(chunk, limit, mut total): (usize, usize, usize),
mut size: Size,
(on_chunk, on_complete): (
impl Fn(usize, usize) + 'static,
impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static,
),
) {
io_stream.input_stream().read_bytes_async(
chunk,
size.chunk,
priority,
Some(&cancellable.clone()),
move |result| match result {
@ -57,19 +60,23 @@ pub fn for_memory_input_stream_async(
// is end of stream
if len == 0 {
return on_complete(Ok((memory_input_stream, total)));
return on_complete(Ok((memory_input_stream, size.total)));
}
// callback chunk function
total += len;
on_chunk(len, total);
size.total += len;
on_chunk(len, size.total);
// push bytes into the memory pool
memory_input_stream.add_bytes(&bytes);
// prevent memory overflow
if total > limit {
return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit)));
if size.total > size.limit {
return on_complete(Err(Error::BytesTotal(
memory_input_stream,
size.total,
size.limit,
)));
}
// handle next chunk..
@ -78,7 +85,7 @@ pub fn for_memory_input_stream_async(
io_stream,
priority,
cancellable,
(chunk, limit, total),
size,
(on_chunk, on_complete),
)
}

View 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,
}
}
}