update Response API

This commit is contained in:
yggverse 2025-02-02 22:12:40 +02:00
parent cdac038135
commit 5358e43697
25 changed files with 1102 additions and 502 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "ggemini"
version = "0.14.1"
version = "0.15.0"
edition = "2021"
license = "MIT"
readme = "README.md"

View file

@ -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| {
from_stream_async(
Vec::with_capacity(HEADER_LEN),
connection.stream(),
cancellable,
priority,
|result| {
callback(match result {
Ok(meta) => Ok(Self { connection, meta }),
Err(e) => Err(Error::Meta(e)),
})
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))),
},
)
}

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

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

View file

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

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

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

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

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

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

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

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

View file

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

View file

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

View file

@ -1 +0,0 @@
// @TODO

View file

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

View file

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

View file

@ -1 +0,0 @@
// @TODO

View file

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

View file

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

View file

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

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

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

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

View file

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