mirror of
https://github.com/YGGverse/ggemini.git
synced 2026-03-31 17:15:31 +00:00
update Response API
This commit is contained in:
parent
cdac038135
commit
5358e43697
25 changed files with 1102 additions and 502 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ggemini"
|
name = "ggemini"
|
||||||
version = "0.14.1"
|
version = "0.15.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,126 @@
|
||||||
//! Read and parse Gemini response as Object
|
//! Read and parse Gemini response as Object
|
||||||
|
|
||||||
pub mod data;
|
pub mod certificate;
|
||||||
|
pub mod data; // @TODO deprecated
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod meta;
|
pub mod failure;
|
||||||
|
pub mod input;
|
||||||
|
pub mod redirect;
|
||||||
|
pub mod success;
|
||||||
|
|
||||||
|
pub use certificate::Certificate;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use meta::Meta;
|
pub use failure::Failure;
|
||||||
|
pub use input::Input;
|
||||||
|
pub use redirect::Redirect;
|
||||||
|
pub use success::Success;
|
||||||
|
|
||||||
use super::Connection;
|
use super::Connection;
|
||||||
use gio::Cancellable;
|
use gio::{Cancellable, IOStream};
|
||||||
use glib::Priority;
|
use glib::{object::IsA, Priority};
|
||||||
|
|
||||||
pub struct Response {
|
const HEADER_LEN: usize = 0x400; // 1024
|
||||||
pub connection: Connection,
|
|
||||||
pub meta: Meta,
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#responses
|
||||||
|
pub enum Response {
|
||||||
|
Input(Input), // 1*
|
||||||
|
Success(Success), // 2*
|
||||||
|
Redirect(Redirect), // 3*
|
||||||
|
Failure(Failure), // 4*,5*
|
||||||
|
Certificate(Certificate), // 6*
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Response {
|
impl Response {
|
||||||
// Constructors
|
/// Asynchronously create new `Self` for given `Connection`
|
||||||
|
|
||||||
/// Create new `Self` from given `Connection`
|
|
||||||
/// * useful for manual [IOStream](https://docs.gtk.org/gio/class.IOStream.html) handle (based on `Meta` bytes pre-parsed)
|
|
||||||
pub fn from_connection_async(
|
pub fn from_connection_async(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
priority: Priority,
|
priority: Priority,
|
||||||
cancellable: Cancellable,
|
cancellable: Cancellable,
|
||||||
callback: impl FnOnce(Result<Self, Error>) + 'static,
|
callback: impl FnOnce(Result<Self, Error>) + 'static,
|
||||||
) {
|
) {
|
||||||
Meta::from_stream_async(connection.stream(), priority, cancellable, |result| {
|
from_stream_async(
|
||||||
callback(match result {
|
Vec::with_capacity(HEADER_LEN),
|
||||||
Ok(meta) => Ok(Self { connection, meta }),
|
connection.stream(),
|
||||||
Err(e) => Err(Error::Meta(e)),
|
cancellable,
|
||||||
})
|
priority,
|
||||||
})
|
|result| {
|
||||||
|
callback(match result {
|
||||||
|
Ok(buffer) => match buffer.first() {
|
||||||
|
Some(byte) => match byte {
|
||||||
|
1 => match Input::from_utf8(&buffer) {
|
||||||
|
Ok(input) => Ok(Self::Input(input)),
|
||||||
|
Err(e) => Err(Error::Input(e)),
|
||||||
|
},
|
||||||
|
2 => match Success::from_utf8(&buffer) {
|
||||||
|
Ok(success) => Ok(Self::Success(success)),
|
||||||
|
Err(e) => Err(Error::Success(e)),
|
||||||
|
},
|
||||||
|
3 => match Redirect::from_utf8(&buffer) {
|
||||||
|
Ok(redirect) => Ok(Self::Redirect(redirect)),
|
||||||
|
Err(e) => Err(Error::Redirect(e)),
|
||||||
|
},
|
||||||
|
4 | 5 => match Failure::from_utf8(&buffer) {
|
||||||
|
Ok(failure) => Ok(Self::Failure(failure)),
|
||||||
|
Err(e) => Err(Error::Failure(e)),
|
||||||
|
},
|
||||||
|
6 => match Certificate::from_utf8(&buffer) {
|
||||||
|
Ok(certificate) => Ok(Self::Certificate(certificate)),
|
||||||
|
Err(e) => Err(Error::Certificate(e)),
|
||||||
|
},
|
||||||
|
b => Err(Error::Code(*b)),
|
||||||
|
},
|
||||||
|
None => Err(Error::Protocol),
|
||||||
|
},
|
||||||
|
Err(e) => Err(e),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
|
||||||
|
/// Asynchronously read header bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
||||||
|
///
|
||||||
|
/// Return UTF-8 buffer collected
|
||||||
|
/// * requires `IOStream` reference to keep `Connection` active in async thread
|
||||||
|
fn from_stream_async(
|
||||||
|
mut buffer: Vec<u8>,
|
||||||
|
stream: impl IsA<IOStream>,
|
||||||
|
cancellable: Cancellable,
|
||||||
|
priority: Priority,
|
||||||
|
on_complete: 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read next byte without record
|
||||||
|
if bytes.contains(&b'\r') {
|
||||||
|
return from_stream_async(buffer, stream, cancellable, priority, on_complete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete without record
|
||||||
|
if bytes.contains(&b'\n') {
|
||||||
|
return on_complete(Ok(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record
|
||||||
|
buffer.append(&mut bytes);
|
||||||
|
|
||||||
|
// Continue
|
||||||
|
from_stream_async(buffer, stream, cancellable, priority, on_complete);
|
||||||
|
}
|
||||||
|
Err((data, e)) => on_complete(Err(Error::Stream(e, data))),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
113
src/client/connection/response/certificate.rs
Normal file
113
src/client/connection/response/certificate.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
/// 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> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
|
||||||
|
NotAuthorized { message: Option<String> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
|
||||||
|
NotValid { message: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Certificate {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
pub fn to_code(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Required { .. } => REQUIRED,
|
||||||
|
Self::NotAuthorized { .. } => NOT_AUTHORIZED,
|
||||||
|
Self::NotValid { .. } => NOT_VALID,
|
||||||
|
}
|
||||||
|
.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Self::Required { message } => message,
|
||||||
|
Self::NotAuthorized { message } => message,
|
||||||
|
Self::NotValid { message } => 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 { message } => message.as_deref().unwrap_or(REQUIRED.1),
|
||||||
|
Self::NotAuthorized { message } => message.as_deref().unwrap_or(NOT_AUTHORIZED.1),
|
||||||
|
Self::NotValid { message } => message.as_deref().unwrap_or(NOT_VALID.1),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
23
src/client/connection/response/certificate/error.rs
Normal file
23
src/client/connection/response/certificate/error.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
str::Utf8Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Code,
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Code => {
|
||||||
|
write!(f, "Status code error")
|
||||||
|
}
|
||||||
|
Self::Utf8Error(e) => {
|
||||||
|
write!(f, "UTF-8 error: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,50 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
str::Utf8Error,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Meta(super::meta::Error),
|
Certificate(super::certificate::Error),
|
||||||
Stream,
|
Code(u8),
|
||||||
|
Failure(super::failure::Error),
|
||||||
|
Input(super::input::Error),
|
||||||
|
Protocol,
|
||||||
|
Redirect(super::redirect::Error),
|
||||||
|
Stream(glib::Error, Vec<u8>),
|
||||||
|
Success(super::success::Error),
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Meta(e) => {
|
Self::Certificate(e) => {
|
||||||
write!(f, "Meta read error: {e}")
|
write!(f, "Certificate error: {e}")
|
||||||
}
|
}
|
||||||
Self::Stream => {
|
Self::Code(e) => {
|
||||||
write!(f, "I/O stream error")
|
write!(f, "Code group error: {e}*")
|
||||||
|
}
|
||||||
|
Self::Failure(e) => {
|
||||||
|
write!(f, "Failure error: {e}")
|
||||||
|
}
|
||||||
|
Self::Input(e) => {
|
||||||
|
write!(f, "Input error: {e}")
|
||||||
|
}
|
||||||
|
Self::Protocol => {
|
||||||
|
write!(f, "Protocol error")
|
||||||
|
}
|
||||||
|
Self::Redirect(e) => {
|
||||||
|
write!(f, "Redirect error: {e}")
|
||||||
|
}
|
||||||
|
Self::Stream(e, ..) => {
|
||||||
|
write!(f, "I/O stream error: {e}")
|
||||||
|
}
|
||||||
|
Self::Success(e) => {
|
||||||
|
write!(f, "Success error: {e}")
|
||||||
|
}
|
||||||
|
Self::Utf8Error(e) => {
|
||||||
|
write!(f, "UTF-8 decode error: {e}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
54
src/client/connection/response/failure.rs
Normal file
54
src/client/connection/response/failure.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
pub mod error;
|
||||||
|
pub mod permanent;
|
||||||
|
pub mod temporary;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
pub use permanent::Permanent;
|
||||||
|
pub use temporary::Temporary;
|
||||||
|
|
||||||
|
pub enum Failure {
|
||||||
|
/// 4* status code group
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure
|
||||||
|
Temporary(Temporary),
|
||||||
|
/// 5* status code group
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure
|
||||||
|
Permanent(Permanent),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Failure {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new `Self` from buffer include header bytes
|
||||||
|
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
|
match buffer.first() {
|
||||||
|
Some(byte) => match byte {
|
||||||
|
4 => match Temporary::from_utf8(buffer) {
|
||||||
|
Ok(input) => Ok(Self::Temporary(input)),
|
||||||
|
Err(e) => Err(Error::Temporary(e)),
|
||||||
|
},
|
||||||
|
5 => match Permanent::from_utf8(buffer) {
|
||||||
|
Ok(failure) => Ok(Self::Permanent(failure)),
|
||||||
|
Err(e) => Err(Error::Permanent(e)),
|
||||||
|
},
|
||||||
|
b => Err(Error::Code(*b)),
|
||||||
|
},
|
||||||
|
None => Err(Error::Protocol),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
pub fn to_code(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Permanent(permanent) => permanent.to_code(),
|
||||||
|
Self::Temporary(temporary) => temporary.to_code(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Self::Permanent(permanent) => permanent.message(),
|
||||||
|
Self::Temporary(temporary) => temporary.message(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/client/connection/response/failure/error.rs
Normal file
28
src/client/connection/response/failure/error.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Code(u8),
|
||||||
|
Permanent(super::permanent::Error),
|
||||||
|
Protocol,
|
||||||
|
Temporary(super::temporary::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Code(e) => {
|
||||||
|
write!(f, "Code group error: {e}*")
|
||||||
|
}
|
||||||
|
Self::Permanent(e) => {
|
||||||
|
write!(f, "Permanent failure group error: {e}")
|
||||||
|
}
|
||||||
|
Self::Protocol => {
|
||||||
|
write!(f, "Protocol error")
|
||||||
|
}
|
||||||
|
Self::Temporary(e) => {
|
||||||
|
write!(f, "Temporary failure group error: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/client/connection/response/failure/permanent.rs
Normal file
169
src/client/connection/response/failure/permanent.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
/// 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> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found
|
||||||
|
NotFound { message: Option<String> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone
|
||||||
|
Gone { message: Option<String> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused
|
||||||
|
ProxyRequestRefused { message: Option<String> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request
|
||||||
|
BadRequest { message: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Permanent {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
.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 { message } => message.as_deref().unwrap_or(DEFAULT.1),
|
||||||
|
Self::NotFound { message } => message.as_deref().unwrap_or(NOT_FOUND.1),
|
||||||
|
Self::Gone { message } => message.as_deref().unwrap_or(GONE.1),
|
||||||
|
Self::ProxyRequestRefused { message } =>
|
||||||
|
message.as_deref().unwrap_or(PROXY_REQUEST_REFUSED.1),
|
||||||
|
Self::BadRequest { message } => message.as_deref().unwrap_or(BAD_REQUEST.1),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
|
||||||
|
let default = Permanent::from_str("50\r\n").unwrap();
|
||||||
|
assert_eq!(default.message(), None);
|
||||||
|
assert_eq!(default.to_code(), DEFAULT.0);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 52
|
||||||
|
let gone = Permanent::from_str("52 Message\r\n").unwrap();
|
||||||
|
assert_eq!(gone.message(), Some("Message"));
|
||||||
|
assert_eq!(gone.to_code(), GONE.0);
|
||||||
|
|
||||||
|
let gone = Permanent::from_str("52\r\n").unwrap();
|
||||||
|
assert_eq!(gone.message(), None);
|
||||||
|
assert_eq!(gone.to_code(), GONE.0);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
23
src/client/connection/response/failure/permanent/error.rs
Normal file
23
src/client/connection/response/failure/permanent/error.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
str::Utf8Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Code,
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Code => {
|
||||||
|
write!(f, "Status code error")
|
||||||
|
}
|
||||||
|
Self::Utf8Error(e) => {
|
||||||
|
write!(f, "UTF-8 error: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/client/connection/response/failure/temporary.rs
Normal file
169
src/client/connection/response/failure/temporary.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
/// 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> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable
|
||||||
|
ServerUnavailable { message: Option<String> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error
|
||||||
|
CgiError { message: Option<String> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error
|
||||||
|
ProxyError { message: Option<String> },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down
|
||||||
|
SlowDown { message: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Temporary {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
.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 { message } => message.as_deref().unwrap_or(DEFAULT.1),
|
||||||
|
Self::ServerUnavailable { message } =>
|
||||||
|
message.as_deref().unwrap_or(SERVER_UNAVAILABLE.1),
|
||||||
|
Self::CgiError { message } => message.as_deref().unwrap_or(CGI_ERROR.1),
|
||||||
|
Self::ProxyError { message } => message.as_deref().unwrap_or(PROXY_ERROR.1),
|
||||||
|
Self::SlowDown { message } => message.as_deref().unwrap_or(SLOW_DOWN.1),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
|
||||||
|
let default = Temporary::from_str("40\r\n").unwrap();
|
||||||
|
assert_eq!(default.message(), None);
|
||||||
|
assert_eq!(default.to_code(), DEFAULT.0);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
23
src/client/connection/response/failure/temporary/error.rs
Normal file
23
src/client/connection/response/failure/temporary/error.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
str::Utf8Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Code,
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Code => {
|
||||||
|
write!(f, "Status code error")
|
||||||
|
}
|
||||||
|
Self::Utf8Error(e) => {
|
||||||
|
write!(f, "UTF-8 error: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/client/connection/response/input.rs
Normal file
109
src/client/connection/response/input.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
const DEFAULT: (u8, &str) = (10, "Input");
|
||||||
|
const SENSITIVE: (u8, &str) = (11, "Sensitive input");
|
||||||
|
|
||||||
|
pub enum Input {
|
||||||
|
Default { message: Option<String> },
|
||||||
|
Sensitive { message: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
.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 { message } => message.as_deref().unwrap_or(DEFAULT.1),
|
||||||
|
Self::Sensitive { message } => message.as_deref().unwrap_or(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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(postfix) = header.strip_prefix("11") {
|
||||||
|
return Ok(Self::Sensitive {
|
||||||
|
message: message(postfix),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(Error::Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
|
||||||
|
fn message(value: &str) -> Option<String> {
|
||||||
|
let value = value.trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_str() {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
// 10
|
||||||
|
let default = Input::from_str("10 Default\r\n").unwrap();
|
||||||
|
assert_eq!(default.to_code(), DEFAULT.0);
|
||||||
|
assert_eq!(default.message(), Some("Default"));
|
||||||
|
assert_eq!(default.to_string(), "Default");
|
||||||
|
|
||||||
|
let default = Input::from_str("10\r\n").unwrap();
|
||||||
|
assert_eq!(default.to_code(), DEFAULT.0);
|
||||||
|
assert_eq!(default.message(), None);
|
||||||
|
assert_eq!(default.to_string(), DEFAULT.1);
|
||||||
|
|
||||||
|
// 11
|
||||||
|
let sensitive = Input::from_str("11 Sensitive\r\n").unwrap();
|
||||||
|
assert_eq!(sensitive.to_code(), SENSITIVE.0);
|
||||||
|
assert_eq!(sensitive.message(), Some("Sensitive"));
|
||||||
|
assert_eq!(sensitive.to_string(), "Sensitive");
|
||||||
|
|
||||||
|
let sensitive = Input::from_str("11\r\n").unwrap();
|
||||||
|
assert_eq!(sensitive.to_code(), SENSITIVE.0);
|
||||||
|
assert_eq!(sensitive.message(), None);
|
||||||
|
assert_eq!(sensitive.to_string(), SENSITIVE.1);
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
str::Utf8Error,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Decode(std::string::FromUtf8Error),
|
|
||||||
Protocol,
|
Protocol,
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Decode(e) => {
|
Self::Utf8Error(e) => {
|
||||||
write!(f, "Decode error: {e}")
|
write!(f, "UTF-8 error: {e}")
|
||||||
}
|
}
|
||||||
Self::Protocol => {
|
Self::Protocol => {
|
||||||
write!(f, "Protocol error")
|
write!(f, "Protocol error")
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
//! Components for reading and parsing meta bytes from response:
|
|
||||||
//! * [Gemini status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
|
||||||
//! * meta data (for interactive statuses like 10, 11, 30 etc)
|
|
||||||
//! * MIME type
|
|
||||||
|
|
||||||
pub mod data;
|
|
||||||
pub mod error;
|
|
||||||
pub mod mime;
|
|
||||||
pub mod status;
|
|
||||||
|
|
||||||
pub use data::Data;
|
|
||||||
pub use error::Error;
|
|
||||||
pub use mime::Mime;
|
|
||||||
pub use status::Status;
|
|
||||||
|
|
||||||
use gio::{
|
|
||||||
prelude::{IOStreamExt, InputStreamExtManual},
|
|
||||||
Cancellable, IOStream,
|
|
||||||
};
|
|
||||||
use glib::{object::IsA, Priority};
|
|
||||||
|
|
||||||
pub const MAX_LEN: usize = 0x400; // 1024
|
|
||||||
|
|
||||||
pub struct Meta {
|
|
||||||
pub status: Status,
|
|
||||||
pub data: Option<Data>,
|
|
||||||
pub mime: Option<Mime>,
|
|
||||||
// @TODO
|
|
||||||
// charset: Option<Charset>,
|
|
||||||
// language: Option<Language>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Meta {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from UTF-8 buffer
|
|
||||||
/// * supports entire response or just meta slice
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
// Calculate buffer length once
|
|
||||||
let len = buffer.len();
|
|
||||||
|
|
||||||
// Parse meta bytes only
|
|
||||||
match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) {
|
|
||||||
Some(slice) => {
|
|
||||||
// Parse data
|
|
||||||
let data = Data::from_utf8(slice);
|
|
||||||
|
|
||||||
if let Err(e) = data {
|
|
||||||
return Err(Error::Data(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
// MIME
|
|
||||||
|
|
||||||
let mime = Mime::from_utf8(slice);
|
|
||||||
|
|
||||||
if let Err(e) = mime {
|
|
||||||
return Err(Error::Mime(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status
|
|
||||||
|
|
||||||
let status = Status::from_utf8(slice);
|
|
||||||
|
|
||||||
if let Err(e) = status {
|
|
||||||
return Err(Error::Status(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
data: data.unwrap(),
|
|
||||||
mime: mime.unwrap(),
|
|
||||||
status: status.unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => Err(Error::Protocol),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
pub fn from_stream_async(
|
|
||||||
stream: impl IsA<IOStream>,
|
|
||||||
priority: Priority,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
on_complete: impl FnOnce(Result<Self, Error>) + 'static,
|
|
||||||
) {
|
|
||||||
read_from_stream_async(
|
|
||||||
Vec::with_capacity(MAX_LEN),
|
|
||||||
stream,
|
|
||||||
cancellable,
|
|
||||||
priority,
|
|
||||||
|result| match result {
|
|
||||||
Ok(buffer) => on_complete(Self::from_utf8(&buffer)),
|
|
||||||
Err(e) => on_complete(Err(e)),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools
|
|
||||||
|
|
||||||
/// Asynchronously read all meta bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html)
|
|
||||||
///
|
|
||||||
/// Return UTF-8 buffer collected
|
|
||||||
/// * require `IOStream` reference to keep `Connection` active in async thread
|
|
||||||
pub fn read_from_stream_async(
|
|
||||||
mut buffer: Vec<u8>,
|
|
||||||
stream: impl IsA<IOStream>,
|
|
||||||
cancellable: Cancellable,
|
|
||||||
priority: Priority,
|
|
||||||
on_complete: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
|
|
||||||
) {
|
|
||||||
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() >= MAX_LEN {
|
|
||||||
return on_complete(Err(Error::Protocol));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read next byte without record
|
|
||||||
if bytes.contains(&b'\r') {
|
|
||||||
return read_from_stream_async(
|
|
||||||
buffer,
|
|
||||||
stream,
|
|
||||||
cancellable,
|
|
||||||
priority,
|
|
||||||
on_complete,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete without record
|
|
||||||
if bytes.contains(&b'\n') {
|
|
||||||
return on_complete(Ok(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record
|
|
||||||
buffer.append(&mut bytes);
|
|
||||||
|
|
||||||
// Continue
|
|
||||||
read_from_stream_async(buffer, stream, cancellable, priority, on_complete);
|
|
||||||
}
|
|
||||||
Err((data, e)) => on_complete(Err(Error::InputStream(data, e))),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
// @TODO
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
//! Components for reading and parsing meta **data** bytes from response
|
|
||||||
//! (e.g. placeholder text for 10, 11, url string for 30, 31 etc)
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
use glib::GString;
|
|
||||||
|
|
||||||
/// Meta **data** holder
|
|
||||||
///
|
|
||||||
/// For example, `value` could contain:
|
|
||||||
/// * placeholder text for 10, 11 status
|
|
||||||
/// * URL string for 30, 31 status
|
|
||||||
pub struct Data(GString);
|
|
||||||
|
|
||||||
impl Data {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Parse meta **data** from UTF-8 buffer
|
|
||||||
/// from entire response or just header slice
|
|
||||||
///
|
|
||||||
/// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
|
||||||
/// that does not expect any data in header
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Option<Self>, Error> {
|
|
||||||
// Define max buffer length for this method
|
|
||||||
const MAX_LEN: usize = 0x400; // 1024
|
|
||||||
|
|
||||||
// Init bytes buffer
|
|
||||||
let mut bytes: Vec<u8> = Vec::with_capacity(MAX_LEN);
|
|
||||||
|
|
||||||
// Calculate len once
|
|
||||||
let len = buffer.len();
|
|
||||||
|
|
||||||
// Skip 3 bytes for status code of `MAX_LEN` expected
|
|
||||||
match buffer.get(3..if len > MAX_LEN { MAX_LEN - 3 } else { len }) {
|
|
||||||
Some(slice) => {
|
|
||||||
for &byte in slice {
|
|
||||||
// End of header
|
|
||||||
if byte == b'\r' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue
|
|
||||||
bytes.push(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes the bytes are valid UTF-8
|
|
||||||
match GString::from_utf8(bytes) {
|
|
||||||
Ok(data) => Ok(match data.is_empty() {
|
|
||||||
false => Some(Self(data)),
|
|
||||||
true => None,
|
|
||||||
}),
|
|
||||||
Err(e) => Err(Error::Decode(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => Err(Error::Protocol),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get `Self` as `std::str`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
self.0.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get `Self` as `glib::GString`
|
|
||||||
pub fn as_gstring(&self) -> &GString {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get `glib::GString` copy of `Self`
|
|
||||||
pub fn to_gstring(&self) -> GString {
|
|
||||||
self.0.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Data {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Data(super::data::Error),
|
|
||||||
InputStream(Vec<u8>, glib::Error),
|
|
||||||
Mime(super::mime::Error),
|
|
||||||
Protocol,
|
|
||||||
Status(super::status::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
match self {
|
|
||||||
Self::Data(e) => {
|
|
||||||
write!(f, "Data error: {e}")
|
|
||||||
}
|
|
||||||
Self::InputStream(_, e) => {
|
|
||||||
// @TODO
|
|
||||||
write!(f, "Input stream error: {e}")
|
|
||||||
}
|
|
||||||
Self::Mime(e) => {
|
|
||||||
write!(f, "MIME error: {e}")
|
|
||||||
}
|
|
||||||
Self::Protocol => {
|
|
||||||
write!(f, "Protocol error")
|
|
||||||
}
|
|
||||||
Self::Status(e) => {
|
|
||||||
write!(f, "Status error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
// @TODO
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
//! MIME type parser for different data types
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
use glib::{Regex, RegexCompileFlags, RegexMatchFlags};
|
|
||||||
|
|
||||||
/// MIME type holder for `Response` (by [Gemtext specification](https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters))
|
|
||||||
/// * the value stored in lowercase
|
|
||||||
pub struct Mime(String);
|
|
||||||
|
|
||||||
impl Mime {
|
|
||||||
// Constructors
|
|
||||||
|
|
||||||
/// Create new `Self` from UTF-8 buffer (that includes **header**)
|
|
||||||
/// * return `None` for non 2* [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Option<Self>, Error> {
|
|
||||||
// Define max buffer length for this method
|
|
||||||
const MAX_LEN: usize = 0x400; // 1024
|
|
||||||
|
|
||||||
// Calculate buffer length once
|
|
||||||
let len = buffer.len();
|
|
||||||
|
|
||||||
// Parse meta bytes only
|
|
||||||
match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) {
|
|
||||||
Some(b) => match std::str::from_utf8(b) {
|
|
||||||
Ok(s) => Self::from_string(s),
|
|
||||||
Err(e) => Err(Error::Decode(e)),
|
|
||||||
},
|
|
||||||
None => Err(Error::Protocol),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new `Self` from `str::str` that includes **header**
|
|
||||||
/// * return `None` for non 2* [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
|
||||||
pub fn from_string(s: &str) -> Result<Option<Self>, Error> {
|
|
||||||
if !s.starts_with("2") {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
match parse(s) {
|
|
||||||
Some(v) => Ok(Some(Self(v))),
|
|
||||||
None => Err(Error::Undefined),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get `Self` as lowercase `std::str`
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
self.0.as_str()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Mime {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract MIME type from from string that includes **header**
|
|
||||||
pub fn parse(s: &str) -> Option<String> {
|
|
||||||
Regex::split_simple(
|
|
||||||
r"^2\d{1}\s([^\/]+\/[^\s;]+)",
|
|
||||||
s,
|
|
||||||
RegexCompileFlags::DEFAULT,
|
|
||||||
RegexMatchFlags::DEFAULT,
|
|
||||||
)
|
|
||||||
.get(1)
|
|
||||||
.map(|this| this.to_lowercase())
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Decode(std::str::Utf8Error),
|
|
||||||
Protocol,
|
|
||||||
Undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Decode(e) => {
|
|
||||||
write!(f, "Decode error: {e}")
|
|
||||||
}
|
|
||||||
Self::Protocol => {
|
|
||||||
write!(f, "Protocol error")
|
|
||||||
}
|
|
||||||
Self::Undefined => {
|
|
||||||
write!(f, "MIME type undefined")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
//! Parser and holder tools for
|
|
||||||
//! [Status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub use error::Error;
|
|
||||||
|
|
||||||
/// Holder for [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Status {
|
|
||||||
// Input
|
|
||||||
Input = 10,
|
|
||||||
SensitiveInput = 11,
|
|
||||||
// Success
|
|
||||||
Success = 20,
|
|
||||||
// Redirect
|
|
||||||
Redirect = 30,
|
|
||||||
PermanentRedirect = 31,
|
|
||||||
// Temporary failure
|
|
||||||
TemporaryFailure = 40,
|
|
||||||
ServerUnavailable = 41,
|
|
||||||
CgiError = 42,
|
|
||||||
ProxyError = 43,
|
|
||||||
SlowDown = 44,
|
|
||||||
// Permanent failure
|
|
||||||
PermanentFailure = 50,
|
|
||||||
NotFound = 51,
|
|
||||||
ResourceGone = 52,
|
|
||||||
ProxyRequestRefused = 53,
|
|
||||||
BadRequest = 59,
|
|
||||||
// Client certificates
|
|
||||||
CertificateRequest = 60,
|
|
||||||
CertificateUnauthorized = 61,
|
|
||||||
CertificateInvalid = 62,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Status {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
match self {
|
|
||||||
Status::Input => "Input",
|
|
||||||
Status::SensitiveInput => "Sensitive Input",
|
|
||||||
Status::Success => "Success",
|
|
||||||
Status::Redirect => "Redirect",
|
|
||||||
Status::PermanentRedirect => "Permanent Redirect",
|
|
||||||
Status::TemporaryFailure => "Temporary Failure",
|
|
||||||
Status::ServerUnavailable => "Server Unavailable",
|
|
||||||
Status::CgiError => "CGI Error",
|
|
||||||
Status::ProxyError => "Proxy Error",
|
|
||||||
Status::SlowDown => "Slow Down",
|
|
||||||
Status::PermanentFailure => "Permanent Failure",
|
|
||||||
Status::NotFound => "Not Found",
|
|
||||||
Status::ResourceGone => "Resource Gone",
|
|
||||||
Status::ProxyRequestRefused => "Proxy Request Refused",
|
|
||||||
Status::BadRequest => "Bad Request",
|
|
||||||
Status::CertificateRequest => "Certificate Request",
|
|
||||||
Status::CertificateUnauthorized => "Certificate Unauthorized",
|
|
||||||
Status::CertificateInvalid => "Certificate Invalid",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Status {
|
|
||||||
/// Create new `Self` from UTF-8 buffer
|
|
||||||
///
|
|
||||||
/// * includes `Self::from_string` parser, it means that given buffer should contain some **header**
|
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
|
||||||
match buffer.get(0..2) {
|
|
||||||
Some(b) => match std::str::from_utf8(b) {
|
|
||||||
Ok(s) => Self::from_string(s),
|
|
||||||
Err(e) => Err(Error::Decode(e)),
|
|
||||||
},
|
|
||||||
None => Err(Error::Protocol),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new `Self` from string that includes **header**
|
|
||||||
pub fn from_string(code: &str) -> Result<Self, Error> {
|
|
||||||
match code {
|
|
||||||
// Input
|
|
||||||
"10" => Ok(Self::Input),
|
|
||||||
"11" => Ok(Self::SensitiveInput),
|
|
||||||
// Success
|
|
||||||
"20" => Ok(Self::Success),
|
|
||||||
// Redirect
|
|
||||||
"30" => Ok(Self::Redirect),
|
|
||||||
"31" => Ok(Self::PermanentRedirect),
|
|
||||||
// Temporary failure
|
|
||||||
"40" => Ok(Self::TemporaryFailure),
|
|
||||||
"41" => Ok(Self::ServerUnavailable),
|
|
||||||
"42" => Ok(Self::CgiError),
|
|
||||||
"43" => Ok(Self::ProxyError),
|
|
||||||
"44" => Ok(Self::SlowDown),
|
|
||||||
// Permanent failure
|
|
||||||
"50" => Ok(Self::PermanentFailure),
|
|
||||||
"51" => Ok(Self::NotFound),
|
|
||||||
"52" => Ok(Self::ResourceGone),
|
|
||||||
"53" => Ok(Self::ProxyRequestRefused),
|
|
||||||
"59" => Ok(Self::BadRequest),
|
|
||||||
// Client certificates
|
|
||||||
"60" => Ok(Self::CertificateRequest),
|
|
||||||
"61" => Ok(Self::CertificateUnauthorized),
|
|
||||||
"62" => Ok(Self::CertificateInvalid),
|
|
||||||
_ => Err(Error::Undefined),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
112
src/client/connection/response/redirect.rs
Normal file
112
src/client/connection/response/redirect.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
use glib::GStringPtr;
|
||||||
|
|
||||||
|
const TEMPORARY: u8 = 30;
|
||||||
|
const PERMANENT: u8 = 31;
|
||||||
|
|
||||||
|
pub enum Redirect {
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
|
||||||
|
Temporary { target: String },
|
||||||
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
|
||||||
|
Permanent { target: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Redirect {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
pub fn target(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Permanent { target } => target,
|
||||||
|
Self::Temporary { target } => target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Redirect {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Permanent { target } => format!("Permanent redirection to `{target}`"),
|
||||||
|
Self::Temporary { target } => format!("Temporary redirection to `{target}`"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_str() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
let permanent = Redirect::from_str("31 /uri\r\n").unwrap();
|
||||||
|
assert_eq!(permanent.target(), "/uri");
|
||||||
|
assert_eq!(permanent.to_code(), PERMANENT);
|
||||||
|
}
|
||||||
27
src/client/connection/response/redirect/error.rs
Normal file
27
src/client/connection/response/redirect/error.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
str::Utf8Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Protocol,
|
||||||
|
Target,
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Utf8Error(e) => {
|
||||||
|
write!(f, "UTF-8 error: {e}")
|
||||||
|
}
|
||||||
|
Self::Protocol => {
|
||||||
|
write!(f, "Protocol error")
|
||||||
|
}
|
||||||
|
Self::Target => {
|
||||||
|
write!(f, "Target error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/client/connection/response/success.rs
Normal file
89
src/client/connection/response/success.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
const DEFAULT: (u8, &str) = (20, "Success");
|
||||||
|
|
||||||
|
pub enum Success {
|
||||||
|
Default { mime: String },
|
||||||
|
// reserved for 2* codes
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Success {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// 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::Default { .. } => DEFAULT.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
pub fn mime(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Default { mime } => mime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_str() {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
let default = Success::from_str("20 text/gemini; charset=utf-8; lang=en\r\n").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(default.mime(), "text/gemini");
|
||||||
|
assert_eq!(default.to_code(), DEFAULT.0);
|
||||||
|
assert_eq!(default.to_string(), DEFAULT.1);
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
use std::fmt::{Display, Formatter, Result};
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
str::Utf8Error,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Decode(std::str::Utf8Error),
|
|
||||||
Protocol,
|
Protocol,
|
||||||
Undefined,
|
Mime,
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Decode(e) => {
|
Self::Utf8Error(e) => {
|
||||||
write!(f, "Decode error: {e}")
|
write!(f, "UTF-8 error: {e}")
|
||||||
}
|
}
|
||||||
Self::Protocol => {
|
Self::Protocol => {
|
||||||
write!(f, "Protocol error")
|
write!(f, "Protocol error")
|
||||||
}
|
}
|
||||||
Self::Undefined => {
|
Self::Mime => {
|
||||||
write!(f, "Undefined")
|
write!(f, "MIME error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue