From a7083852c3bacad886e38b3776c3e3a85fa925a2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Oct 2024 20:45:42 +0300 Subject: [PATCH] initial commit --- Cargo.toml | 6 +- src/client.rs | 10 +++ src/client/connection.rs | 3 + src/client/connection/input_stream.rs | 4 + .../connection/input_stream/byte_buffer.rs | 86 +++++++++++++++++++ .../input_stream/byte_buffer/error.rs | 4 + src/client/error.rs | 8 ++ src/client/response.rs | 42 +++++++++ src/client/response/body.rs | 46 ++++++++++ src/client/response/body/error.rs | 6 ++ src/client/response/error.rs | 4 + src/client/response/header.rs | 70 +++++++++++++++ src/client/response/header/charset.rs | 1 + src/client/response/header/error.rs | 5 ++ src/client/response/header/language.rs | 1 + src/client/response/header/meta.rs | 26 ++++++ src/client/response/header/meta/error.rs | 4 + src/client/response/header/mime.rs | 62 +++++++++++++ src/client/response/header/status.rs | 31 +++++++ src/client/response/header/status/error.rs | 4 + src/client/socket.rs | 23 +++++ src/lib.rs | 15 +--- 22 files changed, 446 insertions(+), 15 deletions(-) create mode 100644 src/client.rs create mode 100644 src/client/connection.rs create mode 100644 src/client/connection/input_stream.rs create mode 100644 src/client/connection/input_stream/byte_buffer.rs create mode 100644 src/client/connection/input_stream/byte_buffer/error.rs create mode 100644 src/client/error.rs create mode 100644 src/client/response.rs create mode 100644 src/client/response/body.rs create mode 100644 src/client/response/body/error.rs create mode 100644 src/client/response/error.rs create mode 100644 src/client/response/header.rs create mode 100644 src/client/response/header/charset.rs create mode 100644 src/client/response/header/error.rs create mode 100644 src/client/response/header/language.rs create mode 100644 src/client/response/header/meta.rs create mode 100644 src/client/response/header/meta/error.rs create mode 100644 src/client/response/header/mime.rs create mode 100644 src/client/response/header/status.rs create mode 100644 src/client/response/header/status/error.rs create mode 100644 src/client/socket.rs diff --git a/Cargo.toml b/Cargo.toml index b1b01a1..1ca9e83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,11 @@ keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"] categories = ["development-tools", "network-programming"] repository = "https://github.com/YGGverse/ggemini" +[dependencies.gio] +package = "gio" +version = "0.20.4" + [dependencies.glib] package = "glib" version = "0.20.4" -#features = ["v2_66"] +features = ["v2_66"] diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..c34d664 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,10 @@ +pub mod connection; +pub mod error; +pub mod response; +pub mod socket; + +pub use error::Error; +pub use response::Response; +pub use socket::Socket; + +// @TODO diff --git a/src/client/connection.rs b/src/client/connection.rs new file mode 100644 index 0000000..5764660 --- /dev/null +++ b/src/client/connection.rs @@ -0,0 +1,3 @@ +pub mod input_stream; + +// @TODO diff --git a/src/client/connection/input_stream.rs b/src/client/connection/input_stream.rs new file mode 100644 index 0000000..faef427 --- /dev/null +++ b/src/client/connection/input_stream.rs @@ -0,0 +1,4 @@ +pub mod byte_buffer; +pub use byte_buffer::ByteBuffer; + +// @TODO diff --git a/src/client/connection/input_stream/byte_buffer.rs b/src/client/connection/input_stream/byte_buffer.rs new file mode 100644 index 0000000..fc7906e --- /dev/null +++ b/src/client/connection/input_stream/byte_buffer.rs @@ -0,0 +1,86 @@ +pub mod error; + +pub use error::Error; + +use gio::{prelude::InputStreamExt, Cancellable, InputStream}; +use glib::{object::IsA, Bytes}; + +pub const DEFAULT_CAPACITY: usize = 0x400; +pub const DEFAULT_CHUNK_SIZE: usize = 0x100; +pub const DEFAULT_MAX_SIZE: usize = 0xfffff; + +pub struct ByteBuffer { + bytes: Vec, +} + +impl ByteBuffer { + /// Create dynamically allocated bytes buffer from `gio::InputStream` + /// + /// Options: + /// * `capacity` bytes request to reduce extra memory overwrites (1024 by default) + /// * `chunk_size` bytes limit to read per iter (256 by default) + /// * `max_size` bytes limit to prevent memory overflow (1M by default) + pub fn from_input_stream( + input_stream: &InputStream, // @TODO + cancellable: Option<&impl IsA>, + capacity: Option, + chunk_size: Option, + max_size: Option, + ) -> Result { + // Create buffer with initial capacity + let mut buffer: Vec = Vec::with_capacity(match capacity { + Some(value) => value, + None => DEFAULT_CAPACITY, + }); + + // Disallow unlimited buffer, use defaults on None + let limit = match max_size { + Some(value) => value, + None => DEFAULT_MAX_SIZE, + }; + + loop { + // Check buffer size to prevent memory overflow + if buffer.len() > limit { + return Err(Error::Overflow); + } + + // Continue bytes reading + match input_stream.read_bytes( + match chunk_size { + Some(value) => value, + None => DEFAULT_CHUNK_SIZE, + }, + cancellable, + ) { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + break; + } + + // Save chunk to buffer + buffer.push(bytes); + } + Err(_) => return Err(Error::Stream), + }; + } + + // Done + Ok(Self { bytes: buffer }) + } + + /// Get link to bytes collected + pub fn bytes(&self) -> &Vec { + &self.bytes + } + + /// Return a copy of the bytes in UTF-8 + pub fn to_utf8(&self) -> Vec { + self.bytes + .iter() + .flat_map(|byte| byte.iter()) + .cloned() + .collect() + } +} diff --git a/src/client/connection/input_stream/byte_buffer/error.rs b/src/client/connection/input_stream/byte_buffer/error.rs new file mode 100644 index 0000000..3a6960e --- /dev/null +++ b/src/client/connection/input_stream/byte_buffer/error.rs @@ -0,0 +1,4 @@ +pub enum Error { + Overflow, + Stream, +} diff --git a/src/client/error.rs b/src/client/error.rs new file mode 100644 index 0000000..4cf662c --- /dev/null +++ b/src/client/error.rs @@ -0,0 +1,8 @@ +pub enum Error { + Close, + Connect, + Input, + Output, + Response, + BufferOverflow, +} diff --git a/src/client/response.rs b/src/client/response.rs new file mode 100644 index 0000000..ce45f1a --- /dev/null +++ b/src/client/response.rs @@ -0,0 +1,42 @@ +pub mod body; +pub mod error; +pub mod header; + +pub use body::Body; +pub use error::Error; +pub use header::Header; + +pub struct Response { + header: Header, + body: Body, +} + +impl Response { + /// Create new `client::Response` + pub fn new(header: Header, body: Body) -> Self { + Self { header, body } + } + + /// Create new `client::Response` from UTF-8 buffer + pub fn from_utf8(buffer: &[u8]) -> Result { + let header = match Header::from_response(buffer) { + Ok(result) => result, + Err(_) => return Err(Error::Header), + }; + + let body = match Body::from_response(buffer) { + Ok(result) => result, + Err(_) => return Err(Error::Body), + }; + + Ok(Self::new(header, body)) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn body(&self) -> &Body { + &self.body + } +} diff --git a/src/client/response/body.rs b/src/client/response/body.rs new file mode 100644 index 0000000..ded304c --- /dev/null +++ b/src/client/response/body.rs @@ -0,0 +1,46 @@ +pub mod error; +pub use error::Error; + +use glib::GString; + +pub struct Body { + buffer: Vec, +} + +impl Body { + /// Construct from response buffer + pub fn from_response(response: &[u8] /* @TODO */) -> Result { + let start = Self::start(response)?; + + let buffer = match response.get(start..) { + Some(result) => result, + None => return Err(Error::Buffer), + }; + + Ok(Self { + buffer: Vec::from(buffer), + }) + } + + // Getters + pub fn buffer(&self) -> &Vec { + &self.buffer + } + + pub fn to_gstring(&self) -> Result { + match GString::from_utf8(self.buffer.to_vec()) { + Ok(result) => Ok(result), + Err(_) => Err(Error::Decode), + } + } + + // Tools + fn start(buffer: &[u8]) -> Result { + for (offset, &byte) in buffer.iter().enumerate() { + if byte == b'\n' { + return Ok(offset + 1); + } + } + Err(Error::Format) + } +} diff --git a/src/client/response/body/error.rs b/src/client/response/body/error.rs new file mode 100644 index 0000000..3284dea --- /dev/null +++ b/src/client/response/body/error.rs @@ -0,0 +1,6 @@ +pub enum Error { + Buffer, + Decode, + Format, + Status, +} diff --git a/src/client/response/error.rs b/src/client/response/error.rs new file mode 100644 index 0000000..089f4ae --- /dev/null +++ b/src/client/response/error.rs @@ -0,0 +1,4 @@ +pub enum Error { + Header, + Body, +} diff --git a/src/client/response/header.rs b/src/client/response/header.rs new file mode 100644 index 0000000..f0af429 --- /dev/null +++ b/src/client/response/header.rs @@ -0,0 +1,70 @@ +pub mod error; +pub mod meta; +pub mod mime; +pub mod status; + +pub use error::Error; +pub use meta::Meta; +pub use mime::Mime; +pub use status::Status; + +pub struct Header { + status: Status, + meta: Option, + mime: Option, + // @TODO + // charset: Option, + // language: Option, +} + +impl Header { + /// Construct from response buffer + /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters + pub fn from_response(response: &[u8] /* @TODO */) -> Result { + let end = Self::end(response)?; + + let buffer = match response.get(..end) { + Some(result) => result, + None => return Err(Error::Buffer), + }; + + let meta = match Meta::from_header(buffer) { + Ok(result) => Some(result), + Err(_) => None, + }; + + let mime = mime::from_header(buffer); // optional + // let charset = charset::from_header(buffer); @TODO + // let language = language::from_header(buffer); @TODO + + let status = match status::from_header(buffer) { + Ok(result) => result, + Err(_) => return Err(Error::Status), + }; + + Ok(Self { status, meta, mime }) + } + + // Getters + pub fn status(&self) -> &Status { + &self.status + } + + pub fn mime(&self) -> &Option { + &self.mime + } + + pub fn meta(&self) -> &Option { + &self.meta + } + + // Tools + fn end(buffer: &[u8]) -> Result { + for (offset, &byte) in buffer.iter().enumerate() { + if byte == b'\r' { + return Ok(offset); + } + } + Err(Error::Format) + } +} diff --git a/src/client/response/header/charset.rs b/src/client/response/header/charset.rs new file mode 100644 index 0000000..1673a59 --- /dev/null +++ b/src/client/response/header/charset.rs @@ -0,0 +1 @@ +// @TODO diff --git a/src/client/response/header/error.rs b/src/client/response/header/error.rs new file mode 100644 index 0000000..b81311b --- /dev/null +++ b/src/client/response/header/error.rs @@ -0,0 +1,5 @@ +pub enum Error { + Buffer, + Format, + Status, +} diff --git a/src/client/response/header/language.rs b/src/client/response/header/language.rs new file mode 100644 index 0000000..1673a59 --- /dev/null +++ b/src/client/response/header/language.rs @@ -0,0 +1 @@ +// @TODO diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs new file mode 100644 index 0000000..dc90ea7 --- /dev/null +++ b/src/client/response/header/meta.rs @@ -0,0 +1,26 @@ +pub mod error; +pub use error::Error; + +use glib::GString; + +pub struct Meta { + buffer: Vec, +} + +impl Meta { + pub fn from_header(buffer: &[u8] /* @TODO */) -> Result { + let buffer = match buffer.get(2..) { + Some(bytes) => bytes.to_vec(), + None => return Err(Error::Undefined), + }; + + Ok(Self { buffer }) + } + + pub fn to_gstring(&self) -> Result { + match GString::from_utf8(self.buffer.clone()) { + Ok(result) => Ok(result), + Err(_) => Err(Error::Undefined), + } + } +} diff --git a/src/client/response/header/meta/error.rs b/src/client/response/header/meta/error.rs new file mode 100644 index 0000000..e0c05eb --- /dev/null +++ b/src/client/response/header/meta/error.rs @@ -0,0 +1,4 @@ +pub enum Error { + Decode, + Undefined, +} diff --git a/src/client/response/header/mime.rs b/src/client/response/header/mime.rs new file mode 100644 index 0000000..1f6ea44 --- /dev/null +++ b/src/client/response/header/mime.rs @@ -0,0 +1,62 @@ +use glib::{GString, Uri}; +use std::path::Path; + +pub enum Mime { + TextGemini, + TextPlain, + ImagePng, + ImageGif, + ImageJpeg, + ImageWebp, +} // @TODO + +pub fn from_header(buffer: &[u8] /* @TODO */) -> Option { + from_string(&match GString::from_utf8(buffer.to_vec()) { + Ok(result) => result, + Err(_) => return None, // @TODO error handler? + }) +} + +pub fn from_path(path: &Path) -> Option { + match path.extension().and_then(|extension| extension.to_str()) { + Some("gmi") | Some("gemini") => Some(Mime::TextGemini), + Some("txt") => Some(Mime::TextPlain), + Some("png") => Some(Mime::ImagePng), + Some("gif") => Some(Mime::ImageGif), + Some("jpeg") | Some("jpg") => Some(Mime::ImageJpeg), + Some("webp") => Some(Mime::ImageWebp), + _ => None, + } +} + +pub fn from_string(value: &str) -> Option { + if value.contains("text/gemini") { + return Some(Mime::TextGemini); + } + + if value.contains("text/plain") { + return Some(Mime::TextPlain); + } + + if value.contains("image/gif") { + return Some(Mime::ImageGif); + } + + if value.contains("image/jpeg") { + return Some(Mime::ImageJpeg); + } + + if value.contains("image/webp") { + return Some(Mime::ImageWebp); + } + + if value.contains("image/png") { + return Some(Mime::ImagePng); + } + + None +} + +pub fn from_uri(uri: &Uri) -> Option { + from_path(Path::new(&uri.to_string())) +} diff --git a/src/client/response/header/status.rs b/src/client/response/header/status.rs new file mode 100644 index 0000000..841460f --- /dev/null +++ b/src/client/response/header/status.rs @@ -0,0 +1,31 @@ +pub mod error; +pub use error::Error; + +use glib::GString; + +/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes +pub enum Status { + Input, + SensitiveInput, + Success, + Redirect, +} // @TODO + +pub fn from_header(buffer: &[u8] /* @TODO */) -> Result { + match buffer.get(0..2) { + Some(bytes) => match GString::from_utf8(bytes.to_vec()) { + Ok(string) => from_string(string.as_str()), + Err(_) => Err(Error::Decode), + }, + None => Err(Error::Undefined), + } +} + +pub fn from_string(code: &str) -> Result { + match code { + "10" => Ok(Status::Input), + "11" => Ok(Status::SensitiveInput), + "20" => Ok(Status::Success), + _ => Err(Error::Undefined), + } +} diff --git a/src/client/response/header/status/error.rs b/src/client/response/header/status/error.rs new file mode 100644 index 0000000..cb85ea9 --- /dev/null +++ b/src/client/response/header/status/error.rs @@ -0,0 +1,4 @@ +pub enum Error { + Undefined, + Decode, +} diff --git a/src/client/socket.rs b/src/client/socket.rs new file mode 100644 index 0000000..1f9bf8f --- /dev/null +++ b/src/client/socket.rs @@ -0,0 +1,23 @@ +use gio::{prelude::SocketClientExt, SocketClient, SocketProtocol, TlsCertificateFlags}; + +pub struct Socket { + gobject: SocketClient, +} + +impl Socket { + /// Create new `gio::SocketClient` preset for Gemini Protocol + pub fn new() -> Self { + let gobject = SocketClient::new(); + + gobject.set_protocol(SocketProtocol::Tcp); + gobject.set_tls_validation_flags(TlsCertificateFlags::INSECURE); + gobject.set_tls(true); + + Self { gobject } + } + + /// Return ref to `gio::SocketClient` GObject + pub fn gobject(&self) -> &SocketClient { + self.gobject.as_ref() + } +} diff --git a/src/lib.rs b/src/lib.rs index b93cf3f..b9babe5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod client;