mirror of
https://github.com/YGGverse/ggemini.git
synced 2026-03-31 17:15:31 +00:00
begin header holder implementation with lazy parser by getters, add request::Mode, add common header_bytes helper
This commit is contained in:
parent
a12a73d311
commit
7c518cecf6
13 changed files with 267 additions and 151 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ggemini"
|
name = "ggemini"
|
||||||
version = "0.17.3"
|
version = "0.18.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ use gio::*;
|
||||||
use glib::*;
|
use glib::*;
|
||||||
|
|
||||||
use ggemini::client::{
|
use ggemini::client::{
|
||||||
connection::{Request, Response},
|
connection::{request::{Mode, Request}, Response},
|
||||||
Client,
|
Client,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -51,6 +51,7 @@ fn main() -> ExitCode {
|
||||||
Client::new().request_async(
|
Client::new().request_async(
|
||||||
Request::Gemini { // or `Request::Titan`
|
Request::Gemini { // or `Request::Titan`
|
||||||
uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(),
|
uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(),
|
||||||
|
mode: Mode::Header // handle content separately (based on MIME)
|
||||||
},
|
},
|
||||||
Priority::DEFAULT,
|
Priority::DEFAULT,
|
||||||
Cancellable::new(),
|
Cancellable::new(),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ pub mod request;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use request::Request;
|
pub use request::{Mode, Request};
|
||||||
pub use response::Response;
|
pub use response::Response;
|
||||||
|
|
||||||
// Local dependencies
|
// Local dependencies
|
||||||
|
|
@ -74,36 +74,42 @@ impl Connection {
|
||||||
Some(&cancellable.clone()),
|
Some(&cancellable.clone()),
|
||||||
move |result| match result {
|
move |result| match result {
|
||||||
Ok(_) => match request {
|
Ok(_) => match request {
|
||||||
Request::Gemini { .. } => Response::from_connection_async(
|
Request::Gemini { mode, .. } => match mode {
|
||||||
self,
|
Mode::All => todo!(),
|
||||||
priority,
|
Mode::Header => Response::header_from_connection_async(
|
||||||
cancellable,
|
self,
|
||||||
|result, connection| {
|
priority,
|
||||||
callback(match result {
|
cancellable,
|
||||||
Ok(response) => Ok((response, connection)),
|
|result, connection| {
|
||||||
Err(e) => Err(Error::Response(e)),
|
callback(match result {
|
||||||
})
|
Ok(response) => Ok((response, connection)),
|
||||||
},
|
Err(e) => Err(Error::Response(e)),
|
||||||
),
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
// Make sure **all data bytes** sent to the destination
|
// Make sure **all data bytes** sent to the destination
|
||||||
// > A partial write is performed with the size of a message block, which is 16kB
|
// > A partial write is performed with the size of a message block, which is 16kB
|
||||||
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes
|
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes
|
||||||
Request::Titan { data, .. } => output_stream.write_all_async(
|
Request::Titan { data, mode, .. } => output_stream.write_all_async(
|
||||||
data,
|
data,
|
||||||
priority,
|
priority,
|
||||||
Some(&cancellable.clone()),
|
Some(&cancellable.clone()),
|
||||||
move |result| match result {
|
move |result| match result {
|
||||||
Ok(_) => Response::from_connection_async(
|
Ok(_) => match mode {
|
||||||
self,
|
Mode::All => todo!(),
|
||||||
priority,
|
Mode::Header => Response::header_from_connection_async(
|
||||||
cancellable,
|
self,
|
||||||
|result, connection| {
|
priority,
|
||||||
callback(match result {
|
cancellable,
|
||||||
Ok(response) => Ok((response, connection)),
|
|result, connection| {
|
||||||
Err(e) => Err(Error::Response(e)),
|
callback(match result {
|
||||||
})
|
Ok(response) => Ok((response, connection)),
|
||||||
},
|
Err(e) => Err(Error::Response(e)),
|
||||||
),
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
Err((b, e)) => callback(Err(Error::Request(b, e))),
|
Err((b, e)) => callback(Err(Error::Request(b, e))),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -124,12 +130,12 @@ impl Connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Tools
|
||||||
|
|
||||||
/// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html)
|
/// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html)
|
||||||
/// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
/// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
|
||||||
/// using `server_identity` as the [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
|
/// using `server_identity` as the [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
|
||||||
pub fn new_tls_client_connection(
|
fn new_tls_client_connection(
|
||||||
socket_connection: &SocketConnection,
|
socket_connection: &SocketConnection,
|
||||||
server_identity: Option<&NetworkAddress>,
|
server_identity: Option<&NetworkAddress>,
|
||||||
is_session_resumption: bool,
|
is_session_resumption: bool,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod mode;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
pub use mode::Mode;
|
||||||
|
|
||||||
// Local dependencies
|
// Local dependencies
|
||||||
|
|
||||||
|
|
@ -10,6 +13,7 @@ use glib::{Bytes, Uri, UriHideFlags};
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
Gemini {
|
Gemini {
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
|
mode: Mode,
|
||||||
},
|
},
|
||||||
Titan {
|
Titan {
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
|
|
@ -18,6 +22,7 @@ pub enum Request {
|
||||||
/// but server MAY reject the request without `mime` value provided.
|
/// but server MAY reject the request without `mime` value provided.
|
||||||
mime: Option<String>,
|
mime: Option<String>,
|
||||||
token: Option<String>,
|
token: Option<String>,
|
||||||
|
mode: Mode,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,12 +32,13 @@ impl Request {
|
||||||
/// Generate header string for `Self`
|
/// Generate header string for `Self`
|
||||||
pub fn header(&self) -> String {
|
pub fn header(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::Gemini { uri } => format!("{uri}\r\n"),
|
Self::Gemini { uri, .. } => format!("{uri}\r\n"),
|
||||||
Self::Titan {
|
Self::Titan {
|
||||||
uri,
|
uri,
|
||||||
data,
|
data,
|
||||||
mime,
|
mime,
|
||||||
token,
|
token,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let mut header = format!(
|
let mut header = format!(
|
||||||
"{};size={}",
|
"{};size={}",
|
||||||
|
|
@ -57,7 +63,7 @@ impl Request {
|
||||||
/// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
/// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
||||||
pub fn uri(&self) -> &Uri {
|
pub fn uri(&self) -> &Uri {
|
||||||
match self {
|
match self {
|
||||||
Self::Gemini { uri } => uri,
|
Self::Gemini { uri, .. } => uri,
|
||||||
Self::Titan { uri, .. } => uri,
|
Self::Titan { uri, .. } => uri,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +85,8 @@ fn test_gemini_header() {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Request::Gemini {
|
Request::Gemini {
|
||||||
uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap()
|
uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(),
|
||||||
|
mode: Mode::Header
|
||||||
}
|
}
|
||||||
.header(),
|
.header(),
|
||||||
format!("{REQUEST}\r\n")
|
format!("{REQUEST}\r\n")
|
||||||
|
|
@ -103,7 +110,8 @@ fn test_titan_header() {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
data: Bytes::from(DATA),
|
data: Bytes::from(DATA),
|
||||||
mime: Some(MIME.to_string()),
|
mime: Some(MIME.to_string()),
|
||||||
token: Some(TOKEN.to_string())
|
token: Some(TOKEN.to_string()),
|
||||||
|
mode: Mode::Header
|
||||||
}
|
}
|
||||||
.header(),
|
.header(),
|
||||||
format!(
|
format!(
|
||||||
|
|
|
||||||
4
src/client/connection/request/mode.rs
Normal file
4
src/client/connection/request/mode.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub enum Mode {
|
||||||
|
Header,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ pub mod redirect;
|
||||||
pub mod success;
|
pub mod success;
|
||||||
|
|
||||||
pub use certificate::Certificate;
|
pub use certificate::Certificate;
|
||||||
pub use error::Error;
|
pub use error::{Error, HeaderBytesError};
|
||||||
pub use failure::Failure;
|
pub use failure::Failure;
|
||||||
pub use input::Input;
|
pub use input::Input;
|
||||||
pub use redirect::Redirect;
|
pub use redirect::Redirect;
|
||||||
|
|
@ -29,13 +29,13 @@ pub enum Response {
|
||||||
|
|
||||||
impl Response {
|
impl Response {
|
||||||
/// Asynchronously create new `Self` for given `Connection`
|
/// Asynchronously create new `Self` for given `Connection`
|
||||||
pub fn from_connection_async(
|
pub fn header_from_connection_async(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
priority: Priority,
|
priority: Priority,
|
||||||
cancellable: Cancellable,
|
cancellable: Cancellable,
|
||||||
callback: impl FnOnce(Result<Self, Error>, Connection) + 'static,
|
callback: impl FnOnce(Result<Self, Error>, Connection) + 'static,
|
||||||
) {
|
) {
|
||||||
from_stream_async(
|
header_from_stream_async(
|
||||||
Vec::with_capacity(HEADER_LEN),
|
Vec::with_capacity(HEADER_LEN),
|
||||||
connection.stream(),
|
connection.stream(),
|
||||||
cancellable,
|
cancellable,
|
||||||
|
|
@ -44,12 +44,12 @@ impl Response {
|
||||||
callback(
|
callback(
|
||||||
match result {
|
match result {
|
||||||
Ok(buffer) => match buffer.first() {
|
Ok(buffer) => match buffer.first() {
|
||||||
Some(byte) => match byte {
|
Some(b) => match b {
|
||||||
b'1' => match Input::from_utf8(&buffer) {
|
b'1' => match Input::from_utf8(&buffer) {
|
||||||
Ok(input) => Ok(Self::Input(input)),
|
Ok(input) => Ok(Self::Input(input)),
|
||||||
Err(e) => Err(Error::Input(e)),
|
Err(e) => Err(Error::Input(e)),
|
||||||
},
|
},
|
||||||
b'2' => match Success::from_utf8(&buffer) {
|
b'2' => match Success::parse(&buffer) {
|
||||||
Ok(success) => Ok(Self::Success(success)),
|
Ok(success) => Ok(Self::Success(success)),
|
||||||
Err(e) => Err(Error::Success(e)),
|
Err(e) => Err(Error::Success(e)),
|
||||||
},
|
},
|
||||||
|
|
@ -65,9 +65,9 @@ impl Response {
|
||||||
Ok(certificate) => Ok(Self::Certificate(certificate)),
|
Ok(certificate) => Ok(Self::Certificate(certificate)),
|
||||||
Err(e) => Err(Error::Certificate(e)),
|
Err(e) => Err(Error::Certificate(e)),
|
||||||
},
|
},
|
||||||
_ => Err(Error::Code),
|
b => Err(Error::Code(*b)),
|
||||||
},
|
},
|
||||||
None => Err(Error::Protocol),
|
None => Err(Error::Protocol(buffer)),
|
||||||
},
|
},
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
},
|
},
|
||||||
|
|
@ -84,43 +84,63 @@ impl Response {
|
||||||
///
|
///
|
||||||
/// Return UTF-8 buffer collected
|
/// Return UTF-8 buffer collected
|
||||||
/// * requires `IOStream` reference to keep `Connection` active in async thread
|
/// * requires `IOStream` reference to keep `Connection` active in async thread
|
||||||
fn from_stream_async(
|
fn header_from_stream_async(
|
||||||
mut buffer: Vec<u8>,
|
mut buffer: Vec<u8>,
|
||||||
stream: impl IsA<IOStream>,
|
stream: impl IsA<IOStream>,
|
||||||
cancellable: Cancellable,
|
cancellable: Cancellable,
|
||||||
priority: Priority,
|
priority: Priority,
|
||||||
on_complete: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
|
callback: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
|
||||||
) {
|
) {
|
||||||
use gio::prelude::{IOStreamExt, InputStreamExtManual};
|
use gio::prelude::{IOStreamExt, InputStreamExtManual};
|
||||||
|
|
||||||
stream.input_stream().read_async(
|
stream.input_stream().read_async(
|
||||||
vec![0],
|
vec![0],
|
||||||
priority,
|
priority,
|
||||||
Some(&cancellable.clone()),
|
Some(&cancellable.clone()),
|
||||||
move |result| match result {
|
move |result| match result {
|
||||||
Ok((mut bytes, size)) => {
|
Ok((bytes, size)) => {
|
||||||
// Expect valid header length
|
if size == 0 {
|
||||||
if size == 0 || buffer.len() >= HEADER_LEN {
|
return callback(Ok(buffer));
|
||||||
return on_complete(Err(Error::Protocol));
|
|
||||||
}
|
}
|
||||||
|
if buffer.len() + bytes.len() > HEADER_LEN {
|
||||||
// Read next byte without record
|
buffer.extend(bytes);
|
||||||
if bytes.contains(&b'\r') {
|
return callback(Err(Error::Protocol(buffer)));
|
||||||
return from_stream_async(buffer, stream, cancellable, priority, on_complete);
|
|
||||||
}
|
}
|
||||||
|
if bytes[0] == b'\r' {
|
||||||
// Complete without record
|
buffer.extend(bytes);
|
||||||
if bytes.contains(&b'\n') {
|
return header_from_stream_async(
|
||||||
return on_complete(Ok(buffer));
|
buffer,
|
||||||
|
stream,
|
||||||
|
cancellable,
|
||||||
|
priority,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
if bytes[0] == b'\n' {
|
||||||
// Record
|
buffer.extend(bytes);
|
||||||
buffer.append(&mut bytes);
|
return callback(Ok(buffer));
|
||||||
|
}
|
||||||
// Continue
|
buffer.extend(bytes);
|
||||||
from_stream_async(buffer, stream, cancellable, priority, on_complete);
|
header_from_stream_async(buffer, stream, cancellable, priority, callback)
|
||||||
}
|
}
|
||||||
Err((data, e)) => on_complete(Err(Error::Stream(e, data))),
|
Err((data, e)) => callback(Err(Error::Stream(e, data))),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get header bytes slice
|
||||||
|
/// * common for all child parsers
|
||||||
|
fn header_bytes(buffer: &[u8]) -> Result<&[u8], HeaderBytesError> {
|
||||||
|
for (i, b) in buffer.iter().enumerate() {
|
||||||
|
if i > 1024 {
|
||||||
|
return Err(HeaderBytesError::Len);
|
||||||
|
}
|
||||||
|
if *b == b'\r' {
|
||||||
|
let n = i + 1;
|
||||||
|
if buffer.get(n).is_some_and(|b| *b == b'\n') {
|
||||||
|
return Ok(&buffer[..n]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(HeaderBytesError::End)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ use std::{
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Certificate(super::certificate::Error),
|
Certificate(super::certificate::Error),
|
||||||
Code,
|
Code(u8),
|
||||||
Failure(super::failure::Error),
|
Failure(super::failure::Error),
|
||||||
Input(super::input::Error),
|
Input(super::input::Error),
|
||||||
Protocol,
|
Protocol(Vec<u8>),
|
||||||
Redirect(super::redirect::Error),
|
Redirect(super::redirect::Error),
|
||||||
Stream(glib::Error, Vec<u8>),
|
Stream(glib::Error, Vec<u8>),
|
||||||
Success(super::success::Error),
|
Success(super::success::Error),
|
||||||
|
|
@ -22,8 +22,8 @@ impl Display for Error {
|
||||||
Self::Certificate(e) => {
|
Self::Certificate(e) => {
|
||||||
write!(f, "Certificate error: {e}")
|
write!(f, "Certificate error: {e}")
|
||||||
}
|
}
|
||||||
Self::Code => {
|
Self::Code(b) => {
|
||||||
write!(f, "Code group error")
|
write!(f, "Unexpected status code byte: {b}")
|
||||||
}
|
}
|
||||||
Self::Failure(e) => {
|
Self::Failure(e) => {
|
||||||
write!(f, "Failure error: {e}")
|
write!(f, "Failure error: {e}")
|
||||||
|
|
@ -31,7 +31,7 @@ impl Display for Error {
|
||||||
Self::Input(e) => {
|
Self::Input(e) => {
|
||||||
write!(f, "Input error: {e}")
|
write!(f, "Input error: {e}")
|
||||||
}
|
}
|
||||||
Self::Protocol => {
|
Self::Protocol(..) => {
|
||||||
write!(f, "Protocol error")
|
write!(f, "Protocol error")
|
||||||
}
|
}
|
||||||
Self::Redirect(e) => {
|
Self::Redirect(e) => {
|
||||||
|
|
@ -49,3 +49,22 @@ impl Display for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum HeaderBytesError {
|
||||||
|
Len,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for HeaderBytesError {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Len => {
|
||||||
|
write!(f, "Unexpected header length")
|
||||||
|
}
|
||||||
|
Self::End => {
|
||||||
|
write!(f, "Unexpected header end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,89 +1,33 @@
|
||||||
|
pub mod default;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
|
pub use default::Default;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
const DEFAULT: (u8, &str) = (20, "Success");
|
pub const CODE: u8 = b'2';
|
||||||
|
|
||||||
pub enum Success {
|
pub enum Success {
|
||||||
Default { mime: String },
|
Default(Default),
|
||||||
// reserved for 2* codes
|
// reserved for 2* codes
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Success {
|
impl Success {
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
/// Create new `Self` from buffer include header bytes
|
/// Parse new `Self` from buffer bytes
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
pub fn parse(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
use std::str::FromStr;
|
if !buffer.first().is_some_and(|b| *b == CODE) {
|
||||||
match std::str::from_utf8(buffer) {
|
return Err(Error::Code);
|
||||||
Ok(header) => Self::from_str(header),
|
|
||||||
Err(e) => Err(Error::Utf8Error(e)),
|
|
||||||
}
|
}
|
||||||
}
|
match Default::parse(&buffer) {
|
||||||
|
Ok(default) => Ok(Self::Default(default)),
|
||||||
// Convertors
|
Err(e) => Err(Error::Default(e)),
|
||||||
|
|
||||||
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]
|
#[test]
|
||||||
fn test_from_str() {
|
fn test() {
|
||||||
use std::str::FromStr;
|
// let default = Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes());
|
||||||
|
todo!()
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
src/client/connection/response/success/default.rs
Normal file
27
src/client/connection/response/success/default.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
pub mod error;
|
||||||
|
pub mod header;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
pub use header::Header;
|
||||||
|
|
||||||
|
pub const CODE: &[u8] = b"20";
|
||||||
|
|
||||||
|
pub struct Default {
|
||||||
|
pub header: Header,
|
||||||
|
pub content: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
pub fn parse(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
|
if !buffer.starts_with(CODE) {
|
||||||
|
return Err(Error::Code);
|
||||||
|
}
|
||||||
|
let header = Header::parse(buffer).map_err(|e| Error::Header(e))?;
|
||||||
|
Ok(Self {
|
||||||
|
content: buffer.get(header.len() + 1..).map(|v| v.to_vec()),
|
||||||
|
header,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/client/connection/response/success/default/error.rs
Normal file
20
src/client/connection/response/success/default/error.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Code,
|
||||||
|
Header(super::header::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Code => {
|
||||||
|
write!(f, "Unexpected status code")
|
||||||
|
}
|
||||||
|
Self::Header(e) => {
|
||||||
|
write!(f, "Header error: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/client/connection/response/success/default/header.rs
Normal file
43
src/client/connection/response/success/default/header.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
pub struct Header(Vec<u8>);
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
pub fn parse(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
|
if !buffer.starts_with(super::CODE) {
|
||||||
|
return Err(Error::Code);
|
||||||
|
}
|
||||||
|
Ok(Self(
|
||||||
|
crate::client::connection::response::header_bytes(buffer)
|
||||||
|
.map_err(|e| Error::Header(e))?
|
||||||
|
.to_vec(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
/// Parse content type for `Self`
|
||||||
|
pub fn mime(&self) -> Result<String, Error> {
|
||||||
|
glib::Regex::split_simple(
|
||||||
|
r"^\d{2}\s([^\/]+\/[^\s;]+)",
|
||||||
|
std::str::from_utf8(&self.0).map_err(|e| Error::Utf8Error(e))?,
|
||||||
|
glib::RegexCompileFlags::DEFAULT,
|
||||||
|
glib::RegexMatchFlags::DEFAULT,
|
||||||
|
)
|
||||||
|
.get(1)
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map_or(Err(Error::Mime), |s| Ok(s.to_lowercase()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
str::Utf8Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Code,
|
||||||
|
Mime,
|
||||||
|
Header(crate::client::connection::response::HeaderBytesError),
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::Code => {
|
||||||
|
write!(f, "Unexpected status code")
|
||||||
|
}
|
||||||
|
Self::Mime => {
|
||||||
|
write!(f, "Unexpected content type")
|
||||||
|
}
|
||||||
|
Self::Header(e) => {
|
||||||
|
write!(f, "Header error: {e}")
|
||||||
|
}
|
||||||
|
Self::Utf8Error(e) => {
|
||||||
|
write!(f, "UTF-8 error: {e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,19 @@
|
||||||
use std::{
|
use std::fmt::{Display, Formatter, Result};
|
||||||
fmt::{Display, Formatter, Result},
|
|
||||||
str::Utf8Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Protocol,
|
Code,
|
||||||
Mime,
|
Default(super::default::Error),
|
||||||
Utf8Error(Utf8Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Utf8Error(e) => {
|
Self::Code => {
|
||||||
write!(f, "UTF-8 error: {e}")
|
write!(f, "Unexpected status code")
|
||||||
}
|
}
|
||||||
Self::Protocol => {
|
Self::Default(e) => {
|
||||||
write!(f, "Protocol error")
|
write!(f, "Header error: {e}")
|
||||||
}
|
|
||||||
Self::Mime => {
|
|
||||||
write!(f, "MIME error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue