mirror of
https://github.com/YGGverse/ggemini.git
synced 2026-03-31 17:15:31 +00:00
Compare commits
57 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11d17e004e | ||
|
|
bba51e38e8 | ||
|
|
0f6eaa563c | ||
|
|
7e9ecf64b3 | ||
|
|
f8537e4ab6 | ||
|
|
5019e66667 | ||
|
|
d8e0a8e35a | ||
|
|
c5d10e020a | ||
|
|
e878fe4ba2 | ||
|
|
cc1018224a | ||
|
|
44196608ce | ||
|
|
bb5b1dfb53 | ||
|
|
c79f386bf1 | ||
|
|
9bbaecf344 | ||
|
|
4dddbd5f8a | ||
|
|
b6ea830545 | ||
|
|
8ee088270f | ||
|
|
46da3a031a | ||
|
|
ea1fb8ea66 | ||
|
|
c9a59e76ee | ||
|
|
e96ff688b3 | ||
|
|
0c75da793f | ||
|
|
064c4107f3 | ||
|
|
d565d56c17 | ||
|
|
3b24625d66 | ||
|
|
f513747e86 | ||
|
|
5229cdae85 | ||
|
|
473ed48715 | ||
|
|
4eb998ef20 | ||
|
|
232531a0bc | ||
|
|
1b96270598 | ||
|
|
845f3dc77e | ||
|
|
e94923ecb5 | ||
|
|
a32eccf5cb | ||
|
|
161142c809 | ||
|
|
8feab6b93b | ||
|
|
5360c6bf19 | ||
|
|
68277f8e83 | ||
|
|
71043bbf73 | ||
|
|
3de096cced | ||
|
|
68e7894125 | ||
|
|
0717e473b7 | ||
|
|
7c518cecf6 | ||
|
|
a12a73d311 | ||
|
|
2102d8887a | ||
|
|
9eb21bb6a3 | ||
|
|
3f968d87b1 | ||
|
|
ab8eb402a8 | ||
|
|
6dbf49cea3 | ||
|
|
b62f990bf2 | ||
|
|
376473660f | ||
|
|
5bb52fbd8c | ||
|
|
af8a972cca | ||
|
|
0aeb501760 | ||
|
|
e6661c1d00 | ||
|
|
fc8356f7ac | ||
|
|
90cc58ab92 |
65 changed files with 2920 additions and 925 deletions
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "ggemini"
|
||||
version = "0.17.1"
|
||||
edition = "2021"
|
||||
version = "0.20.1"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
description = "Glib/Gio-oriented network API for Gemini protocol"
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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!(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ pub mod error;
|
|||
pub use connection::{Connection, Request, Response};
|
||||
pub use error::Error;
|
||||
|
||||
use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate};
|
||||
use gio::{Cancellable, SocketClient, SocketProtocol, TlsCertificate, prelude::SocketClientExt};
|
||||
use glib::Priority;
|
||||
|
||||
// Defaults
|
||||
|
|
@ -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))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt},
|
||||
Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection,
|
||||
prelude::{IOStreamExt, OutputStreamExtManual, TlsCertificateExt, TlsConnectionExt},
|
||||
};
|
||||
use glib::{
|
||||
object::{Cast, ObjectExt},
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,22 +32,23 @@ 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={}",
|
||||
uri.to_string_partial(UriHideFlags::QUERY),
|
||||
data.len()
|
||||
);
|
||||
if let Some(ref mime) = mime {
|
||||
if let Some(mime) = mime {
|
||||
header.push_str(&format!(";mime={mime}"));
|
||||
}
|
||||
if let Some(ref token) = token {
|
||||
if let Some(token) = token {
|
||||
header.push_str(&format!(";token={token}"));
|
||||
}
|
||||
if let Some(query) = uri.query() {
|
||||
|
|
@ -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!(
|
||||
|
|
|
|||
6
src/client/connection/request/mode.rs
Normal file
6
src/client/connection/request/mode.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/// Request modes
|
||||
pub enum Mode {
|
||||
/// Request header bytes only, process content bytes manually
|
||||
/// * useful for manual content type handle: text, stream or large content loaded by chunks
|
||||
HeaderOnly,
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -14,7 +14,7 @@ pub use success::Success;
|
|||
|
||||
use super::Connection;
|
||||
use gio::{Cancellable, IOStream};
|
||||
use glib::{object::IsA, Priority};
|
||||
use glib::{Priority, object::IsA};
|
||||
|
||||
const HEADER_LEN: usize = 1024;
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
79
src/client/connection/response/certificate/not_authorized.rs
Normal file
79
src/client/connection/response/certificate/not_authorized.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code
|
||||
pub const CODE: &[u8] = b"61";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Certificate is not authorized";
|
||||
|
||||
/// Hold header `String` for [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct NotAuthorized(String);
|
||||
|
||||
impl NotAuthorized {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty (not provided by server)
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let na = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(na.message(), Some("Not Authorized"));
|
||||
assert_eq!(na.message_or_default(), "Not Authorized");
|
||||
assert_eq!(na.as_str(), "61 Not Authorized\r\n");
|
||||
assert_eq!(na.as_bytes(), "61 Not Authorized\r\n".as_bytes());
|
||||
|
||||
let na = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(na.message(), None);
|
||||
assert_eq!(na.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(na.as_str(), "61\r\n");
|
||||
assert_eq!(na.as_bytes(), "61\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(NotAuthorized::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(NotAuthorized::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/client/connection/response/certificate/not_valid.rs
Normal file
79
src/client/connection/response/certificate/not_valid.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code
|
||||
pub const CODE: &[u8] = b"62";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Certificate is not valid";
|
||||
|
||||
/// Hold header `String` for [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct NotValid(String);
|
||||
|
||||
impl NotValid {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty (not provided by server)
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let nv = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(nv.message(), Some("Not Valid"));
|
||||
assert_eq!(nv.message_or_default(), "Not Valid");
|
||||
assert_eq!(nv.as_str(), "62 Not Valid\r\n");
|
||||
assert_eq!(nv.as_bytes(), "62 Not Valid\r\n".as_bytes());
|
||||
|
||||
let nv = NotValid::from_utf8("62\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(nv.message(), None);
|
||||
assert_eq!(nv.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(nv.as_str(), "62\r\n");
|
||||
assert_eq!(nv.as_bytes(), "62\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
// @TODO assert!(NotValid::from_utf8("62Fail\r\n".as_bytes()).is_err());
|
||||
assert!(NotValid::from_utf8("63 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(NotValid::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(NotValid::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/client/connection/response/certificate/required.rs
Normal file
79
src/client/connection/response/certificate/required.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code
|
||||
pub const CODE: &[u8] = b"60";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Certificate required";
|
||||
|
||||
/// Hold header `String` for [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct Required(String);
|
||||
|
||||
impl Required {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty (not provided by server)
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let r = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(r.message(), Some("Required"));
|
||||
assert_eq!(r.message_or_default(), "Required");
|
||||
assert_eq!(r.as_str(), "60 Required\r\n");
|
||||
assert_eq!(r.as_bytes(), "60 Required\r\n".as_bytes());
|
||||
|
||||
let r = Required::from_utf8("60\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(r.message(), None);
|
||||
assert_eq!(r.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(r.as_str(), "60\r\n");
|
||||
assert_eq!(r.as_bytes(), "60\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Required::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Required::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
24
src/client/connection/response/certificate/required/error.rs
Normal file
24
src/client/connection/response/certificate/required/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code
|
||||
pub const CODE: &[u8] = b"59";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Bad request";
|
||||
|
||||
/// Hold header `String` for [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct BadRequest(String);
|
||||
|
||||
impl BadRequest {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let br = BadRequest::from_utf8("59 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(br.message(), Some("Message"));
|
||||
assert_eq!(br.message_or_default(), "Message");
|
||||
assert_eq!(br.as_str(), "59 Message\r\n");
|
||||
assert_eq!(br.as_bytes(), "59 Message\r\n".as_bytes());
|
||||
|
||||
let br = BadRequest::from_utf8("59\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(br.message(), None);
|
||||
assert_eq!(br.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(br.as_str(), "59\r\n");
|
||||
assert_eq!(br.as_bytes(), "59\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(BadRequest::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(BadRequest::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(BadRequest::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/client/connection/response/failure/permanent/default.rs
Normal file
78
src/client/connection/response/failure/permanent/default.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code
|
||||
pub const CODE: &[u8] = b"50";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Permanent error";
|
||||
|
||||
/// Hold header `String` for [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct Default(String);
|
||||
|
||||
impl Default {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let d = Default::from_utf8("50 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(d.message(), Some("Message"));
|
||||
assert_eq!(d.message_or_default(), "Message");
|
||||
assert_eq!(d.as_str(), "50 Message\r\n");
|
||||
assert_eq!(d.as_bytes(), "50 Message\r\n".as_bytes());
|
||||
|
||||
let d = Default::from_utf8("50\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(d.message(), None);
|
||||
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(d.as_str(), "50\r\n");
|
||||
assert_eq!(d.as_bytes(), "50\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
78
src/client/connection/response/failure/permanent/gone.rs
Normal file
78
src/client/connection/response/failure/permanent/gone.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code
|
||||
pub const CODE: &[u8] = b"52";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Resource gone";
|
||||
|
||||
/// Hold header `String` for [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct Gone(String);
|
||||
|
||||
impl Gone {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let g = Gone::from_utf8("52 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(g.message(), Some("Message"));
|
||||
assert_eq!(g.message_or_default(), "Message");
|
||||
assert_eq!(g.as_str(), "52 Message\r\n");
|
||||
assert_eq!(g.as_bytes(), "52 Message\r\n".as_bytes());
|
||||
|
||||
let g = Gone::from_utf8("52\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(g.message(), None);
|
||||
assert_eq!(g.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(g.as_str(), "52\r\n");
|
||||
assert_eq!(g.as_bytes(), "52\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(Gone::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Gone::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Gone::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code
|
||||
pub const CODE: &[u8] = b"51";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Not Found";
|
||||
|
||||
/// Hold header `String` for [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct NotFound(String);
|
||||
|
||||
impl NotFound {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let nf = NotFound::from_utf8("51 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(nf.message(), Some("Message"));
|
||||
assert_eq!(nf.message_or_default(), "Message");
|
||||
assert_eq!(nf.as_str(), "51 Message\r\n");
|
||||
assert_eq!(nf.as_bytes(), "51 Message\r\n".as_bytes());
|
||||
|
||||
let nf = NotFound::from_utf8("51\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(nf.message(), None);
|
||||
assert_eq!(nf.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(nf.as_str(), "51\r\n");
|
||||
assert_eq!(nf.as_bytes(), "51\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(NotFound::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(NotFound::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(NotFound::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code
|
||||
pub const CODE: &[u8] = b"53";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Proxy request refused";
|
||||
|
||||
/// Hold header `String` for [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct ProxyRequestRefused(String);
|
||||
|
||||
impl ProxyRequestRefused {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let prf = ProxyRequestRefused::from_utf8("53 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(prf.message(), Some("Message"));
|
||||
assert_eq!(prf.message_or_default(), "Message");
|
||||
assert_eq!(prf.as_str(), "53 Message\r\n");
|
||||
assert_eq!(prf.as_bytes(), "53 Message\r\n".as_bytes());
|
||||
|
||||
let prf = ProxyRequestRefused::from_utf8("53\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(prf.message(), None);
|
||||
assert_eq!(prf.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(prf.as_str(), "53\r\n");
|
||||
assert_eq!(prf.as_bytes(), "53\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(ProxyRequestRefused::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(ProxyRequestRefused::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(ProxyRequestRefused::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code
|
||||
pub const CODE: &[u8] = b"42";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "CGI Error";
|
||||
|
||||
/// Hold header `String` for [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct CgiError(String);
|
||||
|
||||
impl CgiError {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let ce = CgiError::from_utf8("42 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(ce.message(), Some("Message"));
|
||||
assert_eq!(ce.message_or_default(), "Message");
|
||||
assert_eq!(ce.as_str(), "42 Message\r\n");
|
||||
assert_eq!(ce.as_bytes(), "42 Message\r\n".as_bytes());
|
||||
|
||||
let ce = CgiError::from_utf8("42\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(ce.message(), None);
|
||||
assert_eq!(ce.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(ce.as_str(), "42\r\n");
|
||||
assert_eq!(ce.as_bytes(), "42\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(CgiError::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(CgiError::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(CgiError::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/client/connection/response/failure/temporary/default.rs
Normal file
78
src/client/connection/response/failure/temporary/default.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code
|
||||
pub const CODE: &[u8] = b"40";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Temporary error";
|
||||
|
||||
/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct Default(String);
|
||||
|
||||
impl Default {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let d = Default::from_utf8("40 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(d.message(), Some("Message"));
|
||||
assert_eq!(d.message_or_default(), "Message");
|
||||
assert_eq!(d.as_str(), "40 Message\r\n");
|
||||
assert_eq!(d.as_bytes(), "40 Message\r\n".as_bytes());
|
||||
|
||||
let d = Default::from_utf8("40\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(d.message(), None);
|
||||
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(d.as_str(), "40\r\n");
|
||||
assert_eq!(d.as_bytes(), "40\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code
|
||||
pub const CODE: &[u8] = b"43";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Proxy error";
|
||||
|
||||
/// Hold header `String` for [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct ProxyError(String);
|
||||
|
||||
impl ProxyError {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let pe = ProxyError::from_utf8("43 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(pe.message(), Some("Message"));
|
||||
assert_eq!(pe.message_or_default(), "Message");
|
||||
assert_eq!(pe.as_str(), "43 Message\r\n");
|
||||
assert_eq!(pe.as_bytes(), "43 Message\r\n".as_bytes());
|
||||
|
||||
let pe = ProxyError::from_utf8("43\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(pe.message(), None);
|
||||
assert_eq!(pe.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(pe.as_str(), "43\r\n");
|
||||
assert_eq!(pe.as_bytes(), "43\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(ProxyError::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(ProxyError::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(ProxyError::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable)
|
||||
/// temporary error status code
|
||||
pub const CODE: &[u8] = b"41";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Server unavailable";
|
||||
|
||||
/// Hold header `String` for [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable)
|
||||
/// temporary error status code
|
||||
///
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct ServerUnavailable(String);
|
||||
|
||||
impl ServerUnavailable {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let su = ServerUnavailable::from_utf8("41 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(su.message(), Some("Message"));
|
||||
assert_eq!(su.message_or_default(), "Message");
|
||||
assert_eq!(su.as_str(), "41 Message\r\n");
|
||||
assert_eq!(su.as_bytes(), "41 Message\r\n".as_bytes());
|
||||
|
||||
let su = ServerUnavailable::from_utf8("41\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(su.message(), None);
|
||||
assert_eq!(su.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(su.as_str(), "41\r\n");
|
||||
assert_eq!(su.as_bytes(), "41\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(ServerUnavailable::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(ServerUnavailable::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(ServerUnavailable::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Slow Down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down)
|
||||
/// temporary error status code
|
||||
pub const CODE: &[u8] = b"44";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Slow down";
|
||||
|
||||
/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down)
|
||||
/// temporary error status code
|
||||
///
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct SlowDown(String);
|
||||
|
||||
impl SlowDown {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let sd = SlowDown::from_utf8("44 Message\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(sd.message(), Some("Message"));
|
||||
assert_eq!(sd.message_or_default(), "Message");
|
||||
assert_eq!(sd.as_str(), "44 Message\r\n");
|
||||
assert_eq!(sd.as_bytes(), "44 Message\r\n".as_bytes());
|
||||
|
||||
let sd = SlowDown::from_utf8("44\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(sd.message(), None);
|
||||
assert_eq!(sd.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(sd.as_str(), "44\r\n");
|
||||
assert_eq!(sd.as_bytes(), "44\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(SlowDown::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(SlowDown::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(SlowDown::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
78
src/client/connection/response/input/default.rs
Normal file
78
src/client/connection/response/input/default.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code
|
||||
pub const CODE: &[u8] = b"10";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Input expected";
|
||||
|
||||
/// Hold header `String` for [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct Default(String);
|
||||
|
||||
impl Default {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let d = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(d.message(), Some("Default"));
|
||||
assert_eq!(d.message_or_default(), "Default");
|
||||
assert_eq!(d.as_str(), "10 Default\r\n");
|
||||
assert_eq!(d.as_bytes(), "10 Default\r\n".as_bytes());
|
||||
|
||||
let d = Default::from_utf8("10\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(d.message(), None);
|
||||
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(d.as_str(), "10\r\n");
|
||||
assert_eq!(d.as_bytes(), "10\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Default::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
24
src/client/connection/response/input/default/error.rs
Normal file
24
src/client/connection/response/input/default/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
78
src/client/connection/response/input/sensitive.rs
Normal file
78
src/client/connection/response/input/sensitive.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code
|
||||
pub const CODE: &[u8] = b"11";
|
||||
|
||||
/// Default message if the optional value was not provided by the server
|
||||
/// * useful to skip match cases in external applications,
|
||||
/// by using `super::message_or_default` method.
|
||||
pub const DEFAULT_MESSAGE: &str = "Sensitive input expected";
|
||||
|
||||
/// Hold header `String` for [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct Sensitive(String);
|
||||
|
||||
impl Sensitive {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * return `None` if the message is empty
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty())
|
||||
}
|
||||
|
||||
/// Get optional message for `Self`
|
||||
/// * if the optional message not provided by the server, return `DEFAULT_MESSAGE`
|
||||
pub fn message_or_default(&self) -> &str {
|
||||
self.message().unwrap_or(DEFAULT_MESSAGE)
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
// ok
|
||||
let s = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(s.message(), Some("Sensitive"));
|
||||
assert_eq!(s.message_or_default(), "Sensitive");
|
||||
assert_eq!(s.as_str(), "11 Sensitive\r\n");
|
||||
assert_eq!(s.as_bytes(), "11 Sensitive\r\n".as_bytes());
|
||||
|
||||
let s = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(s.message(), None);
|
||||
assert_eq!(s.message_or_default(), DEFAULT_MESSAGE);
|
||||
assert_eq!(s.as_str(), "11\r\n");
|
||||
assert_eq!(s.as_bytes(), "11\r\n".as_bytes());
|
||||
|
||||
// err
|
||||
assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err());
|
||||
assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err());
|
||||
}
|
||||
24
src/client/connection/response/input/sensitive/error.rs
Normal file
24
src/client/connection/response/input/sensitive/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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/");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
src/client/connection/response/redirect/permanent.rs
Normal file
82
src/client/connection/response/redirect/permanent.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
// Local dependencies
|
||||
|
||||
use glib::Uri;
|
||||
|
||||
/// [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code
|
||||
pub const CODE: &[u8] = b"31";
|
||||
|
||||
/// Hold header `String` for [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct Permanent(String);
|
||||
|
||||
impl Permanent {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get raw target for `Self`
|
||||
/// * return `Err` if the required target is empty
|
||||
pub fn target(&self) -> Result<&str, Error> {
|
||||
self.0
|
||||
.get(2..)
|
||||
.map(|s| s.trim())
|
||||
.filter(|x| !x.is_empty())
|
||||
.ok_or(Error::TargetEmpty)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
pub fn uri(&self, base: &Uri) -> Result<Uri, Error> {
|
||||
super::uri(self.target()?, base).map_err(Error::Uri)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
const BUFFER: &str = "31 gemini://geminiprotocol.net/path\r\n";
|
||||
let bytes = BUFFER.as_bytes();
|
||||
let base = Uri::build(
|
||||
glib::UriFlags::NONE,
|
||||
"gemini",
|
||||
None,
|
||||
Some("geminiprotocol.net"),
|
||||
-1,
|
||||
"/path/",
|
||||
Some("query"),
|
||||
Some("fragment"),
|
||||
);
|
||||
let permanent = Permanent::from_utf8(bytes).unwrap();
|
||||
assert_eq!(permanent.as_str(), BUFFER);
|
||||
assert_eq!(permanent.as_bytes(), bytes);
|
||||
assert!(permanent.target().is_ok());
|
||||
assert!(
|
||||
permanent
|
||||
.uri(&base)
|
||||
.is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path")
|
||||
);
|
||||
assert!(Permanent::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err());
|
||||
}
|
||||
32
src/client/connection/response/redirect/permanent/error.rs
Normal file
32
src/client/connection/response/redirect/permanent/error.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
TargetEmpty,
|
||||
Uri(super::super::UriError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::TargetEmpty => {
|
||||
write!(f, "Expected target is empty")
|
||||
}
|
||||
Self::Uri(e) => {
|
||||
write!(f, "URI parse error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/client/connection/response/redirect/temporary.rs
Normal file
82
src/client/connection/response/redirect/temporary.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
// Local dependencies
|
||||
|
||||
use glib::Uri;
|
||||
|
||||
/// [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code
|
||||
pub const CODE: &[u8] = b"30";
|
||||
|
||||
/// Hold header `String` for [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code
|
||||
/// * this response type does not contain body data
|
||||
/// * the header member is closed to require valid construction
|
||||
pub struct Temporary(String);
|
||||
|
||||
impl Temporary {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get raw target for `Self`
|
||||
/// * return `Err` if the required target is empty
|
||||
pub fn target(&self) -> Result<&str, Error> {
|
||||
self.0
|
||||
.get(2..)
|
||||
.map(|s| s.trim())
|
||||
.filter(|x| !x.is_empty())
|
||||
.ok_or(Error::TargetEmpty)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
pub fn uri(&self, base: &Uri) -> Result<Uri, Error> {
|
||||
super::uri(self.target()?, base).map_err(Error::Uri)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
const BUFFER: &str = "30 gemini://geminiprotocol.net/path\r\n";
|
||||
let bytes = BUFFER.as_bytes();
|
||||
let base = Uri::build(
|
||||
glib::UriFlags::NONE,
|
||||
"gemini",
|
||||
None,
|
||||
Some("geminiprotocol.net"),
|
||||
-1,
|
||||
"/path/",
|
||||
Some("query"),
|
||||
Some("fragment"),
|
||||
);
|
||||
let temporary = Temporary::from_utf8(BUFFER.as_bytes()).unwrap();
|
||||
assert_eq!(temporary.as_str(), BUFFER);
|
||||
assert_eq!(temporary.as_bytes(), bytes);
|
||||
assert!(temporary.target().is_ok());
|
||||
assert!(
|
||||
temporary
|
||||
.uri(&base)
|
||||
.is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path")
|
||||
);
|
||||
assert!(Temporary::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err())
|
||||
}
|
||||
32
src/client/connection/response/redirect/temporary/error.rs
Normal file
32
src/client/connection/response/redirect/temporary/error.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
TargetEmpty,
|
||||
Uri(super::super::UriError),
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::TargetEmpty => {
|
||||
write!(f, "Expected target is empty")
|
||||
}
|
||||
Self::Uri(e) => {
|
||||
write!(f, "URI parse error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 decode error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
51
src/client/connection/response/success/default.rs
Normal file
51
src/client/connection/response/success/default.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
pub mod error;
|
||||
pub mod header;
|
||||
|
||||
pub use error::Error;
|
||||
pub use header::Header;
|
||||
|
||||
/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code
|
||||
pub const CODE: &[u8] = b"20";
|
||||
|
||||
/// Holder for [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code
|
||||
/// * this response type MAY contain body data
|
||||
/// * the header has closed members to require valid construction
|
||||
pub struct Default {
|
||||
/// Formatted header holder with additional API
|
||||
pub header: Header,
|
||||
/// Default success response MAY include body data
|
||||
/// * if the `Request` constructed with `Mode::HeaderOnly` flag,\
|
||||
/// this value wants to be processed manually, using external application logic (specific for content-type)
|
||||
pub content: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Default {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
let header = Header::from_utf8(buffer).map_err(Error::Header)?;
|
||||
Ok(Self {
|
||||
content: buffer
|
||||
.get(header.as_bytes().len()..)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map_or(Vec::new(), |v| v.to_vec()),
|
||||
header,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let d = Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap();
|
||||
assert_eq!(d.header.mime().unwrap(), "text/gemini");
|
||||
assert!(d.content.is_empty());
|
||||
|
||||
let d =
|
||||
Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\ndata".as_bytes()).unwrap();
|
||||
assert_eq!(d.header.mime().unwrap(), "text/gemini");
|
||||
assert_eq!(d.content.len(), 4);
|
||||
}
|
||||
20
src/client/connection/response/success/default/error.rs
Normal file
20
src/client/connection/response/success/default/error.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Header(super::header::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/client/connection/response/success/default/header.rs
Normal file
60
src/client/connection/response/success/default/header.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
pub struct Header(String);
|
||||
|
||||
impl Header {
|
||||
// Constructors
|
||||
|
||||
/// Parse `Self` from buffer contains header bytes
|
||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||
if !buffer.starts_with(super::CODE) {
|
||||
return Err(Error::Code);
|
||||
}
|
||||
Ok(Self(
|
||||
std::str::from_utf8(
|
||||
crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?,
|
||||
)
|
||||
.map_err(Error::Utf8Error)?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Parse content type for `Self`
|
||||
pub fn mime(&self) -> Result<String, Error> {
|
||||
glib::Regex::split_simple(
|
||||
r"^\d{2}\s([^\/]+\/[^\s;]+)",
|
||||
&self.0,
|
||||
glib::RegexCompileFlags::DEFAULT,
|
||||
glib::RegexMatchFlags::DEFAULT,
|
||||
)
|
||||
.get(1)
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map_or(Err(Error::Mime), |s| Ok(s.to_lowercase()))
|
||||
}
|
||||
|
||||
/// Get header bytes of `Self`
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
/// Get header string of `Self`
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let s = "20 text/gemini; charset=utf-8; lang=en\r\n";
|
||||
let b = s.as_bytes();
|
||||
let h = Header::from_utf8(b).unwrap();
|
||||
assert_eq!(h.mime().unwrap(), "text/gemini");
|
||||
assert_eq!(h.as_bytes(), b);
|
||||
assert_eq!(h.as_str(), s);
|
||||
|
||||
assert!(Header::from_utf8("21 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err());
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
str::Utf8Error,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Code,
|
||||
Mime,
|
||||
Header(crate::client::connection::response::HeaderBytesError),
|
||||
Utf8Error(Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Code => {
|
||||
write!(f, "Unexpected status code")
|
||||
}
|
||||
Self::Mime => {
|
||||
write!(f, "Unexpected content type")
|
||||
}
|
||||
Self::Header(e) => {
|
||||
write!(f, "Header error: {e}")
|
||||
}
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 error: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
pub mod error;
|
||||
pub mod size;
|
||||
|
||||
pub use error::Error;
|
||||
pub use size::Size;
|
||||
|
||||
use gio::{
|
||||
prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual},
|
||||
Cancellable, FileOutputStream, IOStream,
|
||||
prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual},
|
||||
};
|
||||
use glib::{object::IsA, Bytes, Priority};
|
||||
use glib::{Bytes, Priority, object::IsA};
|
||||
|
||||
/// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
||||
/// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html)
|
||||
|
|
@ -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))),
|
||||
},
|
||||
|
|
|
|||
17
src/gio/file_output_stream/size.rs
Normal file
17
src/gio/file_output_stream/size.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/// Mutable bytes count
|
||||
pub struct Size {
|
||||
pub chunk: usize,
|
||||
/// `None` for unlimited
|
||||
pub limit: Option<usize>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
impl Default for Size {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chunk: 0x10000, // 64KB
|
||||
limit: None,
|
||||
total: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
pub mod error;
|
||||
pub mod size;
|
||||
|
||||
pub use error::Error;
|
||||
pub use size::Size;
|
||||
|
||||
use gio::{
|
||||
prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt},
|
||||
Cancellable, IOStream, MemoryInputStream,
|
||||
prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt},
|
||||
};
|
||||
use glib::{object::IsA, Priority};
|
||||
use glib::{Priority, object::IsA};
|
||||
|
||||
/// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html)
|
||||
/// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
16
src/gio/memory_input_stream/size.rs
Normal file
16
src/gio/memory_input_stream/size.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/// Mutable bytes count
|
||||
pub struct Size {
|
||||
pub chunk: usize,
|
||||
pub limit: usize,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
impl Default for Size {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chunk: 0x10000, // 64KB
|
||||
limit: 0xfffff, // 1 MB
|
||||
total: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue