update response namespace

This commit is contained in:
yggverse 2024-12-01 08:50:28 +02:00
parent 70fc128c29
commit 4767929050
19 changed files with 59 additions and 50 deletions

View file

@ -2,12 +2,20 @@ use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Response(crate::client::connection::response::Error),
Stream(glib::Error),
TlsClientConnection(glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Stream(e) => {
write!(f, "TLS client connection error: {e}")
}
Self::Response(e) => {
write!(f, "Response error: {e}")
}
Self::TlsClientConnection(e) => {
write!(f, "TLS client connection error: {e}")
}

View file

@ -0,0 +1,37 @@
//! Read and parse Gemini response as Object
pub mod data;
pub mod error;
pub mod meta;
pub use error::Error;
pub use meta::Meta;
use super::Connection;
use gio::Cancellable;
use glib::Priority;
pub struct Response {
pub connection: Connection,
pub meta: Meta,
}
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)
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)),
})
})
}
}

View file

@ -0,0 +1,7 @@
//! Gemini response could have different MIME type for data.
//! Use one of components below to parse response according to content type expected.
//!
//! * MIME type could be detected using `client::response::Meta` parser
pub mod text;
pub use text::Text;

View file

@ -0,0 +1,111 @@
//! Tools for Text-based response
pub mod error;
pub use error::Error;
// Local dependencies
use gio::{
prelude::{IOStreamExt, InputStreamExt},
Cancellable, IOStream,
};
use glib::{object::IsA, GString, Priority};
// Default limits
pub const BUFFER_CAPACITY: usize = 0x400; // 1024
pub const BUFFER_MAX_SIZE: usize = 0xfffff; // 1M
/// Container for text-based response data
pub struct Text {
pub data: GString,
}
impl Default for Text {
fn default() -> Self {
Self::new()
}
}
impl Text {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
data: GString::new(),
}
}
/// Create new `Self` from string
pub fn from_string(data: &str) -> Self {
Self { data: data.into() }
}
/// Create new `Self` from UTF-8 buffer
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match GString::from_utf8(buffer.into()) {
Ok(data) => Ok(Self::from_string(&data)),
Err(e) => Err(Error::Decode(e)),
}
}
/// 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_all_from_stream_async(
Vec::with_capacity(BUFFER_CAPACITY),
stream,
cancellable,
priority,
|result| match result {
Ok(buffer) => on_complete(Self::from_utf8(&buffer)),
Err(e) => on_complete(Err(e)),
},
);
}
}
// Tools
/// Asynchronously read all 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_all_from_stream_async(
mut buffer: Vec<u8>,
stream: impl IsA<IOStream>,
cancelable: Cancellable,
priority: Priority,
callback: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
) {
stream.input_stream().read_bytes_async(
BUFFER_CAPACITY,
priority,
Some(&cancelable.clone()),
move |result| match result {
Ok(bytes) => {
// No bytes were read, end of stream
if bytes.len() == 0 {
return callback(Ok(buffer));
}
// Validate overflow
if buffer.len() + bytes.len() > BUFFER_MAX_SIZE {
return callback(Err(Error::BufferOverflow));
}
// Save chunks to buffer
for &byte in bytes.iter() {
buffer.push(byte);
}
// Continue bytes reading
read_all_from_stream_async(buffer, stream, cancelable, priority, callback);
}
Err(e) => callback(Err(Error::InputStream(e))),
},
);
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
BufferOverflow,
Decode(std::string::FromUtf8Error),
InputStream(glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::BufferOverflow => {
write!(f, "Buffer overflow")
}
Self::Decode(e) => {
write!(f, "Decode error: {e}")
}
Self::InputStream(e) => {
write!(f, "Input stream read error: {e}")
}
}
}
}

View file

@ -0,0 +1,20 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Meta(super::meta::Error),
Stream,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Meta(e) => {
write!(f, "Meta read error: {e}")
}
Self::Stream => {
write!(f, "I/O stream error")
}
}
}
}

View file

@ -0,0 +1,147 @@
//! 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

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

View file

@ -0,0 +1,61 @@
//! 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 {
pub value: 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(value) => Ok(match value.is_empty() {
false => Some(Self { value }),
true => None,
}),
Err(e) => Err(Error::Decode(e)),
}
}
None => Err(Error::Protocol),
}
}
}

View file

@ -0,0 +1,20 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Decode(std::string::FromUtf8Error),
Protocol,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Decode(e) => {
write!(f, "Decode error: {e}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
}
}
}

View file

@ -0,0 +1,33 @@
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

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

View file

@ -0,0 +1,142 @@
//! MIME type parser for different data types:
//!
//! * UTF-8 buffer with entire response or just with meta slice (that include entire **header**)
//! * String (that include **header**)
//! * [Uri](https://docs.gtk.org/glib/struct.Uri.html) (that include **extension**)
//! * `std::Path` (that include **extension**)
pub mod error;
pub use error::Error;
use glib::{GString, Uri};
use std::path::Path;
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
#[derive(Debug)]
pub enum Mime {
// Text
TextGemini,
TextPlain,
// Image
ImageGif,
ImageJpeg,
ImagePng,
ImageSvg,
ImageWebp,
// Audio
AudioFlac,
AudioMpeg,
AudioOgg,
} // @TODO
impl Mime {
/// Create new `Self` from UTF-8 buffer (that includes **header**)
///
/// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
/// that does not expect MIME type in header
/// * includes `Self::from_string` parser,
/// it means that given buffer should contain some **header** (not filepath or any other type of strings)
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(value) => match GString::from_utf8(value.into()) {
Ok(string) => Self::from_string(string.as_str()),
Err(e) => Err(Error::Decode(e)),
},
None => Err(Error::Protocol),
}
}
/// Create new `Self` from `std::Path` that includes file **extension**
pub fn from_path(path: &Path) -> Result<Self, Error> {
match path.extension().and_then(|extension| extension.to_str()) {
// Text
Some("gmi" | "gemini") => Ok(Self::TextGemini),
Some("txt") => Ok(Self::TextPlain),
// Image
Some("gif") => Ok(Self::ImageGif),
Some("jpeg" | "jpg") => Ok(Self::ImageJpeg),
Some("png") => Ok(Self::ImagePng),
Some("svg") => Ok(Self::ImageSvg),
Some("webp") => Ok(Self::ImageWebp),
// Audio
Some("flac") => Ok(Self::AudioFlac),
Some("mp3") => Ok(Self::AudioMpeg),
Some("oga" | "ogg" | "opus" | "spx") => Ok(Self::AudioOgg),
_ => Err(Error::Undefined),
} // @TODO extension to lowercase
}
/// Create new `Self` from string that includes **header**
///
/// **Return**
///
/// * `None` if MIME type not found
/// * `Error::Undefined` if status code 2* and type not found in `Mime` enum
pub fn from_string(value: &str) -> Result<Option<Self>, Error> {
// Text
if value.contains("text/gemini") {
return Ok(Some(Self::TextGemini));
}
if value.contains("text/plain") {
return Ok(Some(Self::TextPlain));
}
// Image
if value.contains("image/gif") {
return Ok(Some(Self::ImageGif));
}
if value.contains("image/jpeg") {
return Ok(Some(Self::ImageJpeg));
}
if value.contains("image/png") {
return Ok(Some(Self::ImagePng));
}
if value.contains("image/svg+xml") {
return Ok(Some(Self::ImageSvg));
}
if value.contains("image/webp") {
return Ok(Some(Self::ImageWebp));
}
// Audio
if value.contains("audio/flac") {
return Ok(Some(Self::AudioFlac));
}
if value.contains("audio/mpeg") {
return Ok(Some(Self::AudioMpeg));
}
if value.contains("audio/ogg") {
return Ok(Some(Self::AudioOgg));
}
// Some type exist, but not defined yet (on status code is 2*)
if value.starts_with("2") && value.contains("/") {
return Err(Error::Undefined);
}
// Done
Ok(None) // may be empty (status code ^2*)
}
/// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
/// that includes file **extension**
pub fn from_uri(uri: &Uri) -> Result<Self, Error> {
Self::from_path(Path::new(&uri.to_string()))
}
}

View file

@ -0,0 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Decode(std::string::FromUtf8Error),
Protocol,
Undefined,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Decode(e) => {
write!(f, "Decode error: {e}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
Self::Undefined => {
write!(f, "Undefined error")
}
}
}
}

View file

@ -0,0 +1,82 @@
//! Parser and holder tools for
//! [Status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes)
pub mod error;
pub use error::Error;
use glib::GString;
/// 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 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(value) => match GString::from_utf8(value.to_vec()) {
Ok(string) => Self::from_string(string.as_str()),
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,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Decode(std::string::FromUtf8Error),
Protocol,
Undefined,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Decode(e) => {
write!(f, "Decode error: {e}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
Self::Undefined => {
write!(f, "Undefined error")
}
}
}
}