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]
|
||||
name = "ggemini"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -1,37 +1,126 @@
|
|||
//! Read and parse Gemini response as Object
|
||||
|
||||
pub mod data;
|
||||
pub mod certificate;
|
||||
pub mod data; // @TODO deprecated
|
||||
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 meta::Meta;
|
||||
pub use failure::Failure;
|
||||
pub use input::Input;
|
||||
pub use redirect::Redirect;
|
||||
pub use success::Success;
|
||||
|
||||
use super::Connection;
|
||||
use gio::Cancellable;
|
||||
use glib::Priority;
|
||||
use gio::{Cancellable, IOStream};
|
||||
use glib::{object::IsA, Priority};
|
||||
|
||||
pub struct Response {
|
||||
pub connection: Connection,
|
||||
pub meta: Meta,
|
||||
const HEADER_LEN: usize = 0x400; // 1024
|
||||
|
||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#responses
|
||||
pub enum Response {
|
||||
Input(Input), // 1*
|
||||
Success(Success), // 2*
|
||||
Redirect(Redirect), // 3*
|
||||
Failure(Failure), // 4*,5*
|
||||
Certificate(Certificate), // 6*
|
||||
}
|
||||
|
||||
impl Response {
|
||||
// Constructors
|
||||
|
||||
/// 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)
|
||||
/// Asynchronously create new `Self` for given `Connection`
|
||||
pub fn from_connection_async(
|
||||
connection: Connection,
|
||||
priority: Priority,
|
||||
cancellable: Cancellable,
|
||||
callback: impl FnOnce(Result<Self, Error>) + 'static,
|
||||
) {
|
||||
Meta::from_stream_async(connection.stream(), priority, cancellable, |result| {
|
||||
callback(match result {
|
||||
Ok(meta) => Ok(Self { connection, meta }),
|
||||
Err(e) => Err(Error::Meta(e)),
|
||||
})
|
||||
})
|
||||
from_stream_async(
|
||||
Vec::with_capacity(HEADER_LEN),
|
||||
connection.stream(),
|
||||
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)]
|
||||
pub enum Error {
|
||||
Meta(super::meta::Error),
|
||||
Stream,
|
||||
Certificate(super::certificate::Error),
|
||||
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 {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Meta(e) => {
|
||||
write!(f, "Meta read error: {e}")
|
||||
Self::Certificate(e) => {
|
||||
write!(f, "Certificate error: {e}")
|
||||
}
|
||||
Self::Stream => {
|
||||
write!(f, "I/O stream error")
|
||||
Self::Code(e) => {
|
||||
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)]
|
||||
pub enum Error {
|
||||
Decode(std::string::FromUtf8Error),
|
||||
Protocol,
|
||||
Utf8Error(Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Decode(e) => {
|
||||
write!(f, "Decode error: {e}")
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 error: {e}")
|
||||
}
|
||||
Self::Protocol => {
|
||||
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)]
|
||||
pub enum Error {
|
||||
Decode(std::str::Utf8Error),
|
||||
Protocol,
|
||||
Undefined,
|
||||
Mime,
|
||||
Utf8Error(Utf8Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
match self {
|
||||
Self::Decode(e) => {
|
||||
write!(f, "Decode error: {e}")
|
||||
Self::Utf8Error(e) => {
|
||||
write!(f, "UTF-8 error: {e}")
|
||||
}
|
||||
Self::Protocol => {
|
||||
write!(f, "Protocol error")
|
||||
}
|
||||
Self::Undefined => {
|
||||
write!(f, "Undefined")
|
||||
Self::Mime => {
|
||||
write!(f, "MIME error")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue