From 605c98e553d241041c569a93d8ebb82f8b877fbd Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Oct 2024 21:00:27 +0300 Subject: [PATCH 001/392] reorder asc --- src/client/response/header/status/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/response/header/status/error.rs b/src/client/response/header/status/error.rs index cb85ea9..e0c05eb 100644 --- a/src/client/response/header/status/error.rs +++ b/src/client/response/header/status/error.rs @@ -1,4 +1,4 @@ pub enum Error { - Undefined, Decode, + Undefined, } From 00a91d7c0cd34c5a76e58ca787ac87c7129f6059 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Oct 2024 11:01:40 +0300 Subject: [PATCH 002/392] make status optional --- Cargo.toml | 2 +- src/client/response/header.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 265c506..e3f9df8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.1.1" +version = "0.1.2" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/client/response/header.rs b/src/client/response/header.rs index f0af429..ef3da60 100644 --- a/src/client/response/header.rs +++ b/src/client/response/header.rs @@ -9,7 +9,7 @@ pub use mime::Mime; pub use status::Status; pub struct Header { - status: Status, + status: Option, meta: Option, mime: Option, // @TODO @@ -38,15 +38,15 @@ impl Header { // let language = language::from_header(buffer); @TODO let status = match status::from_header(buffer) { - Ok(result) => result, - Err(_) => return Err(Error::Status), + Ok(result) => Some(result), + Err(_) => None, }; Ok(Self { status, meta, mime }) } // Getters - pub fn status(&self) -> &Status { + pub fn status(&self) -> &Option { &self.status } From e8381892ef8cddf6b52be1ce9a29734524967123 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Oct 2024 12:49:33 +0300 Subject: [PATCH 003/392] change version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e3f9df8..1a88bcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.1.2" +version = "0.2.0" edition = "2021" license = "MIT" readme = "README.md" From 62c0f5ea83a43f62c5076cb11abbc30427e9b9ee Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 24 Oct 2024 15:36:47 +0300 Subject: [PATCH 004/392] add new constructors, add read_input_stream_async method, change read_input_stream api --- .../connection/input_stream/byte_buffer.rs | 108 +++++++++++++++--- .../input_stream/byte_buffer/error.rs | 3 +- 2 files changed, 92 insertions(+), 19 deletions(-) diff --git a/src/client/connection/input_stream/byte_buffer.rs b/src/client/connection/input_stream/byte_buffer.rs index fc7906e..2e23b5d 100644 --- a/src/client/connection/input_stream/byte_buffer.rs +++ b/src/client/connection/input_stream/byte_buffer.rs @@ -3,7 +3,7 @@ pub mod error; pub use error::Error; use gio::{prelude::InputStreamExt, Cancellable, InputStream}; -use glib::{object::IsA, Bytes}; +use glib::{object::IsA, Bytes, Priority}; pub const DEFAULT_CAPACITY: usize = 0x400; pub const DEFAULT_CHUNK_SIZE: usize = 0x100; @@ -14,25 +14,42 @@ pub struct ByteBuffer { } impl ByteBuffer { - /// Create dynamically allocated bytes buffer from `gio::InputStream` + // Constructors + + /// Create new dynamically allocated bytes buffer with default capacity + pub fn new() -> Self { + Self::with_capacity(Some(DEFAULT_CAPACITY)) + } + + /// Create new dynamically allocated bytes buffer with initial capacity /// /// Options: - /// * `capacity` bytes request to reduce extra memory overwrites (1024 by default) + /// * initial bytes request to reduce extra memory overwrites (1024 by default) + pub fn with_capacity(value: Option) -> Self { + Self { + bytes: Vec::with_capacity(match value { + Some(capacity) => capacity, + None => DEFAULT_CAPACITY, + }), + } + } + + // Readers + + /// Populate bytes buffer synchronously from `gio::InputStream` + /// + /// Options: + /// * `input_stream` https://docs.gtk.org/gio/class.InputStream.html + /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html /// * `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 + pub fn read_input_stream( + mut self, + input_stream: InputStream, 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, @@ -41,7 +58,7 @@ impl ByteBuffer { loop { // Check buffer size to prevent memory overflow - if buffer.len() > limit { + if self.bytes.len() > limit { return Err(Error::Overflow); } @@ -56,18 +73,73 @@ impl ByteBuffer { Ok(bytes) => { // No bytes were read, end of stream if bytes.len() == 0 { - break; + return Ok(self); } // Save chunk to buffer - buffer.push(bytes); + self.bytes.push(bytes); } - Err(_) => return Err(Error::Stream), + Err(_) => return Err(Error::StreamChunkRead), }; } + } - // Done - Ok(Self { bytes: buffer }) + /// Populate bytes buffer asynchronously from `gio::InputStream`, + /// apply callback function to `Ok(Self)` on success + /// + /// Options: + /// * `input_stream` https://docs.gtk.org/gio/class.InputStream.html + /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html + /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html + /// * `chunk_size` optional bytes limit to read per iter (256 by default) + /// * `max_size` optional bytes limit to prevent memory overflow (1M by default) + /// * `callback` user function to apply on complete + pub fn read_input_stream_async( + mut self, + input_stream: InputStream, + cancellable: Cancellable, + priority: Priority, + chunk_size: Option, + max_size: Option, + callback: impl FnOnce(Result) + 'static, + ) { + // Clone reference counted chunk dependencies + let _input_stream = input_stream.clone(); + let _cancellable = cancellable.clone(); + + // Continue bytes reading + input_stream.read_bytes_async( + match max_size { + Some(value) => value, + None => DEFAULT_MAX_SIZE, + }, + priority, + Some(&cancellable), + move |result| -> () { + match result { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return callback(Ok(self)); + } + + // Save chunk to buffer + self.bytes.push(bytes); + + // Continue bytes reading... + self.read_input_stream_async( + _input_stream, + _cancellable, + priority, + chunk_size, + max_size, + callback, + ); + } + Err(_) => callback(Err(Error::StreamChunkReadAsync)), + } + }, + ); } /// Get link to bytes collected diff --git a/src/client/connection/input_stream/byte_buffer/error.rs b/src/client/connection/input_stream/byte_buffer/error.rs index 3a6960e..bab66ff 100644 --- a/src/client/connection/input_stream/byte_buffer/error.rs +++ b/src/client/connection/input_stream/byte_buffer/error.rs @@ -1,4 +1,5 @@ pub enum Error { Overflow, - Stream, + StreamChunkRead, + StreamChunkReadAsync, } From ee476e56d2092a96ad6884ccd23e6d329333067d Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 24 Oct 2024 15:40:46 +0300 Subject: [PATCH 005/392] change version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1a88bcb..eb01db0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.2.0" +version = "0.3.0" edition = "2021" license = "MIT" readme = "README.md" From 496e9811389c2f3993c6a4ccc00e0be50cec2c81 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 24 Oct 2024 20:22:44 +0300 Subject: [PATCH 006/392] delegate buffer features to input level, delete byte_buffer, input_stream mods as deprecated --- src/client/connection.rs | 4 +- src/client/connection/input.rs | 156 +++++++++++++++++ src/client/connection/input/buffer.rs | 87 ++++++++++ .../byte_buffer => input/buffer}/error.rs | 0 src/client/connection/input/error.rs | 6 + src/client/connection/input_stream.rs | 4 - .../connection/input_stream/byte_buffer.rs | 158 ------------------ 7 files changed, 252 insertions(+), 163 deletions(-) create mode 100644 src/client/connection/input.rs create mode 100644 src/client/connection/input/buffer.rs rename src/client/connection/{input_stream/byte_buffer => input/buffer}/error.rs (100%) create mode 100644 src/client/connection/input/error.rs delete mode 100644 src/client/connection/input_stream.rs delete mode 100644 src/client/connection/input_stream/byte_buffer.rs diff --git a/src/client/connection.rs b/src/client/connection.rs index 5764660..7c2e44f 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,3 +1,5 @@ -pub mod input_stream; +pub mod input; + +pub use input::Input; // @TODO diff --git a/src/client/connection/input.rs b/src/client/connection/input.rs new file mode 100644 index 0000000..288d4e1 --- /dev/null +++ b/src/client/connection/input.rs @@ -0,0 +1,156 @@ +pub mod buffer; +pub mod error; + +pub use buffer::Buffer; +pub use error::Error; + +use gio::{prelude::InputStreamExt, Cancellable, InputStream}; +use glib::Priority; + +// Defaults +pub const DEFAULT_READ_CHUNK: usize = 0x100; + +pub struct Input { + buffer: Buffer, + stream: InputStream, +} + +impl Input { + // Constructors + + /// Create new `Input` from `gio::InputStream` + /// + /// https://docs.gtk.org/gio/class.InputStream.html + pub fn new_from_stream(stream: InputStream) -> Self { + Self { + buffer: Buffer::new(), + stream, + } + } + + // Actions + + /// Synchronously read all bytes from `gio::InputStream` to `input::Buffer` + /// + /// Options: + /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html + /// * `chunk` max bytes to read per chunk (256 by default) + pub fn read_all( + mut self, + cancelable: Option, + chunk: Option, + ) -> Result { + loop { + // Continue bytes reading + match self.stream.read_bytes( + match chunk { + Some(value) => value, + None => DEFAULT_READ_CHUNK, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + ) { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return Ok(self.buffer); + } + + // Save chunk to buffer + match self.buffer.push(bytes) { + Ok(_) => continue, + Err(buffer::Error::Overflow) => return Err(Error::BufferOverflow), + Err(_) => return Err(Error::BufferWrite), + }; + } + Err(_) => return Err(Error::StreamChunkRead), + }; + } + } + + /// Asynchronously read all bytes from `gio::InputStream` to `input::Buffer`, + /// + /// applies `callback` function on last byte reading complete. + /// + /// Options: + /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) + /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) + /// * `chunk` optional max bytes to read per chunk (`DEFAULT_READ_CHUNK` by default) + /// * `callback` user function to apply on async iteration complete + pub fn read_all_async( + mut self, + cancelable: Option, + priority: Option, + chunk: Option, + callback: impl FnOnce(Result) + 'static, + ) { + // Continue bytes reading + self.stream.clone().read_bytes_async( + match chunk { + Some(value) => value, + None => DEFAULT_READ_CHUNK, + }, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + move |result| { + match result { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return callback(Ok(self.buffer)); + } + + // Save chunk to buffer + match self.buffer.push(bytes) { + Err(buffer::Error::Overflow) => { + return callback(Err(Error::BufferOverflow)) + } + + // Other errors related to write issues @TODO test + Err(_) => return callback(Err(Error::BufferWrite)), + + // Async function, nothing to return yet + _ => (), + }; + + // Continue bytes reading... + self.read_all_async(cancelable, priority, chunk, callback); + } + Err(_) => callback(Err(Error::StreamChunkReadAsync)), + } + }, + ); + } + + // Setters + + pub fn set_buffer(&mut self, buffer: Buffer) { + self.buffer = buffer; + } + + pub fn set_stream(&mut self, stream: InputStream) { + self.stream = stream; + } + + // Getters + + /// Get reference to `Buffer` + pub fn buffer(&self) -> &Buffer { + &self.buffer + } + + /// Get reference to `gio::InputStream` + pub fn stream(&self) -> &InputStream { + &self.stream + } +} diff --git a/src/client/connection/input/buffer.rs b/src/client/connection/input/buffer.rs new file mode 100644 index 0000000..b9264c7 --- /dev/null +++ b/src/client/connection/input/buffer.rs @@ -0,0 +1,87 @@ +pub mod error; +pub use error::Error; + +use glib::Bytes; + +pub const DEFAULT_CAPACITY: usize = 0x400; +pub const DEFAULT_MAX_SIZE: usize = 0xfffff; + +pub struct Buffer { + bytes: Vec, + max_size: usize, +} + +impl Buffer { + // Constructors + + /// Create new dynamically allocated `Buffer` with default `capacity` and `max_size` limit + pub fn new() -> Self { + Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) + } + + /// Create new dynamically allocated `Buffer` with options + /// + /// Options: + /// * `capacity` initial bytes request to reduce extra memory overwrites (1024 by default) + /// * `max_size` max bytes to prevent memory overflow (1M by default) + pub fn new_with_options(capacity: Option, max_size: Option) -> Self { + Self { + bytes: Vec::with_capacity(match capacity { + Some(value) => value, + None => DEFAULT_CAPACITY, + }), + max_size: match max_size { + Some(value) => value, + None => DEFAULT_MAX_SIZE, + }, + } + } + + // Setters + + /// Set new `Buffer.max_size` value to prevent memory overflow + /// + /// Use `DEFAULT_MAX_SIZE` if `None` given. + pub fn set_max_size(&mut self, value: Option) { + self.max_size = match value { + Some(size) => size, + None => DEFAULT_MAX_SIZE, + } + } + + // Actions + + /// Push `glib::Bytes` to `Buffer.bytes` + /// + /// Return `Error::Overflow` on `Buffer.max_size` reached. + pub fn push(&mut self, bytes: Bytes) -> Result { + // Calculate new size value + let total = self.bytes.len() + bytes.len(); + + // Validate overflow + if total > self.max_size { + return Err(Error::Overflow); + } + + // Success + self.bytes.push(bytes); + + Ok(total) + } + + // Getters + + /// Get reference to bytes collected + pub fn bytes(&self) -> &Vec { + &self.bytes + } + + /// Return copy of bytes as UTF-8 vector + 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/buffer/error.rs similarity index 100% rename from src/client/connection/input_stream/byte_buffer/error.rs rename to src/client/connection/input/buffer/error.rs diff --git a/src/client/connection/input/error.rs b/src/client/connection/input/error.rs new file mode 100644 index 0000000..4bcff86 --- /dev/null +++ b/src/client/connection/input/error.rs @@ -0,0 +1,6 @@ +pub enum Error { + BufferOverflow, + BufferWrite, + StreamChunkRead, + StreamChunkReadAsync, +} diff --git a/src/client/connection/input_stream.rs b/src/client/connection/input_stream.rs deleted file mode 100644 index faef427..0000000 --- a/src/client/connection/input_stream.rs +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 2e23b5d..0000000 --- a/src/client/connection/input_stream/byte_buffer.rs +++ /dev/null @@ -1,158 +0,0 @@ -pub mod error; - -pub use error::Error; - -use gio::{prelude::InputStreamExt, Cancellable, InputStream}; -use glib::{object::IsA, Bytes, Priority}; - -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 { - // Constructors - - /// Create new dynamically allocated bytes buffer with default capacity - pub fn new() -> Self { - Self::with_capacity(Some(DEFAULT_CAPACITY)) - } - - /// Create new dynamically allocated bytes buffer with initial capacity - /// - /// Options: - /// * initial bytes request to reduce extra memory overwrites (1024 by default) - pub fn with_capacity(value: Option) -> Self { - Self { - bytes: Vec::with_capacity(match value { - Some(capacity) => capacity, - None => DEFAULT_CAPACITY, - }), - } - } - - // Readers - - /// Populate bytes buffer synchronously from `gio::InputStream` - /// - /// Options: - /// * `input_stream` https://docs.gtk.org/gio/class.InputStream.html - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html - /// * `chunk_size` bytes limit to read per iter (256 by default) - /// * `max_size` bytes limit to prevent memory overflow (1M by default) - pub fn read_input_stream( - mut self, - input_stream: InputStream, - cancellable: Option<&impl IsA>, - chunk_size: Option, - max_size: Option, - ) -> Result { - // 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 self.bytes.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 { - return Ok(self); - } - - // Save chunk to buffer - self.bytes.push(bytes); - } - Err(_) => return Err(Error::StreamChunkRead), - }; - } - } - - /// Populate bytes buffer asynchronously from `gio::InputStream`, - /// apply callback function to `Ok(Self)` on success - /// - /// Options: - /// * `input_stream` https://docs.gtk.org/gio/class.InputStream.html - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html - /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html - /// * `chunk_size` optional bytes limit to read per iter (256 by default) - /// * `max_size` optional bytes limit to prevent memory overflow (1M by default) - /// * `callback` user function to apply on complete - pub fn read_input_stream_async( - mut self, - input_stream: InputStream, - cancellable: Cancellable, - priority: Priority, - chunk_size: Option, - max_size: Option, - callback: impl FnOnce(Result) + 'static, - ) { - // Clone reference counted chunk dependencies - let _input_stream = input_stream.clone(); - let _cancellable = cancellable.clone(); - - // Continue bytes reading - input_stream.read_bytes_async( - match max_size { - Some(value) => value, - None => DEFAULT_MAX_SIZE, - }, - priority, - Some(&cancellable), - move |result| -> () { - match result { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return callback(Ok(self)); - } - - // Save chunk to buffer - self.bytes.push(bytes); - - // Continue bytes reading... - self.read_input_stream_async( - _input_stream, - _cancellable, - priority, - chunk_size, - max_size, - callback, - ); - } - Err(_) => callback(Err(Error::StreamChunkReadAsync)), - } - }, - ); - } - - /// 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() - } -} From f73ef32547f5f5492c8d5583f073dd5b561de02c Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 24 Oct 2024 20:23:06 +0300 Subject: [PATCH 007/392] change version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eb01db0..f366386 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.3.0" +version = "0.4.0" edition = "2021" license = "MIT" readme = "README.md" From 015c3ad7cacd702b15232d911afee8a4744b289e Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 24 Oct 2024 20:46:35 +0300 Subject: [PATCH 008/392] update readme --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a583402..c450e5c 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,12 @@ Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! -> \ No newline at end of file +> + +## Integrations + +* [Yoda](https://github.com/YGGverse/Yoda) - Browser for Gemini Protocol + +## See also + +* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file From 9793a67187472308496e5e985bd096a57a423455 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 03:20:23 +0300 Subject: [PATCH 009/392] draft request high-level api --- src/client.rs | 35 +++- src/client/error.rs | 7 +- src/client/socket.rs | 57 ++++++- src/client/socket/connection.rs | 115 +++++++++++++ src/client/socket/connection/error.rs | 7 + src/client/socket/connection/input.rs | 155 ++++++++++++++++++ src/client/socket/connection/input/buffer.rs | 87 ++++++++++ .../socket/connection/input/buffer/error.rs | 4 + src/client/socket/connection/input/error.rs | 5 + src/client/socket/connection/output.rs | 71 ++++++++ src/client/socket/connection/output/error.rs | 3 + src/client/socket/error.rs | 3 + 12 files changed, 533 insertions(+), 16 deletions(-) create mode 100644 src/client/socket/connection.rs create mode 100644 src/client/socket/connection/error.rs create mode 100644 src/client/socket/connection/input.rs create mode 100644 src/client/socket/connection/input/buffer.rs create mode 100644 src/client/socket/connection/input/buffer/error.rs create mode 100644 src/client/socket/connection/input/error.rs create mode 100644 src/client/socket/connection/output.rs create mode 100644 src/client/socket/connection/output/error.rs create mode 100644 src/client/socket/error.rs diff --git a/src/client.rs b/src/client.rs index c34d664..3158ff2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,10 +1,41 @@ -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 +use gio::Cancellable; +use glib::{Priority, Uri}; + +/// High-level API to make async request, get `Response` and close connection. +pub fn request_async( + uri: Uri, + cancelable: Option, + priority: Option, + callback: impl FnOnce(Result) + 'static, +) { + // Create new socket connection + Socket::new().connect_async( + uri.clone(), + cancelable.clone(), + move |connect| match connect { + Ok(connection) => { + connection.request_async(uri, cancelable, priority, None, |request| { + callback(match request { + Ok(buffer) => match Response::from_utf8(&buffer) { + Ok(response) => Ok(response), + Err(_) => Err(Error::Response), + }, + Err(_) => Err(Error::Request), + }); + + //connection.close_async(cancelable, priority, |_| {}); // @TODO + }) + } + Err(_) => callback(Err(Error::Connection)), + }, + ); +} diff --git a/src/client/error.rs b/src/client/error.rs index 4cf662c..d14b8f1 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -1,8 +1,5 @@ pub enum Error { - Close, - Connect, - Input, - Output, + Connection, Response, - BufferOverflow, + Request, } diff --git a/src/client/socket.rs b/src/client/socket.rs index 1f9bf8f..1146cf1 100644 --- a/src/client/socket.rs +++ b/src/client/socket.rs @@ -1,23 +1,62 @@ -use gio::{prelude::SocketClientExt, SocketClient, SocketProtocol, TlsCertificateFlags}; +pub mod connection; +pub mod error; + +pub use connection::Connection; +pub use error::Error; + +pub const DEFAULT_PORT: u16 = 1965; + +use gio::{ + prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificateFlags, +}; +use glib::Uri; pub struct Socket { - gobject: SocketClient, + client: SocketClient, } impl Socket { + // Constructors + /// Create new `gio::SocketClient` preset for Gemini Protocol pub fn new() -> Self { - let gobject = SocketClient::new(); + let client = SocketClient::new(); - gobject.set_protocol(SocketProtocol::Tcp); - gobject.set_tls_validation_flags(TlsCertificateFlags::INSECURE); - gobject.set_tls(true); + client.set_protocol(SocketProtocol::Tcp); + client.set_tls_validation_flags(TlsCertificateFlags::INSECURE); + client.set_tls(true); - Self { gobject } + Self { client } } + // Actions + pub fn connect_async( + &self, + uri: Uri, + cancelable: Option, + callback: impl FnOnce(Result) + 'static, + ) { + self.client.connect_to_uri_async( + uri.to_str().as_str(), + DEFAULT_PORT, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + |result| { + callback(match result { + Ok(connection) => Ok(Connection::new_from(connection)), + Err(_) => Err(Error::Connection), + }) + }, + ); + } + + // Getters + /// Return ref to `gio::SocketClient` GObject - pub fn gobject(&self) -> &SocketClient { - self.gobject.as_ref() + pub fn client(&self) -> &SocketClient { + self.client.as_ref() } } diff --git a/src/client/socket/connection.rs b/src/client/socket/connection.rs new file mode 100644 index 0000000..b9b6e2f --- /dev/null +++ b/src/client/socket/connection.rs @@ -0,0 +1,115 @@ +pub mod error; +pub mod input; +pub mod output; + +pub use error::Error; +pub use input::Input; +pub use output::Output; + +use gio::{prelude::IOStreamExt, Cancellable, SocketConnection}; +use glib::{gformat, Bytes, Priority, Uri}; + +pub struct Connection { + connection: SocketConnection, +} + +impl Connection { + // Constructors + + pub fn new_from(connection: SocketConnection) -> Self { + Self { connection } + } + + // Actions + + /// MIddle-level API to make single async socket request for current connection: + /// + /// 1. send `Uri` request to the ouput stream; + /// 2. write entire input stream into the new `Vec` buffer on success; + /// 3. close current connection if callback function does not prevent that action by return. + pub fn request_async( + self, + uri: Uri, + cancelable: Option, + priority: Option, + chunk: Option, + callback: impl FnOnce(Result, Error>) + 'static, + ) { + // Send request + Output::new_from_stream(self.connection.output_stream()).write_async( + &Bytes::from(gformat!("{}\r\n", uri.to_str()).as_bytes()), + cancelable.clone(), + priority, + move |output| match output { + Ok(_) => { + // Read response + Input::new_from_stream(self.connection.input_stream()).read_all_async( + cancelable.clone(), + priority, + chunk, + move |input| { + // Apply callback function + callback(match input { + Ok(buffer) => Ok(buffer.to_utf8()), + Err(error) => Err(match error { + input::Error::BufferOverflow => Error::InputBufferOverflow, + input::Error::BufferWrite => Error::InputBufferWrite, + input::Error::StreamChunkRead => Error::InputStreamChunkRead, + }), + }); + + // Close connection if callback act does not prevent that + self.close_async(cancelable, priority, |_| {}); // @TODO + }, + ); + } + Err(error) => { + // Apply callback function + callback(Err(match error { + output::Error::StreamWrite => Error::OutputStreamWrite, + })); + + // Close connection if callback act does not prevent that + self.close_async(cancelable, priority, |_| {}); // @TODO + } + }, + ); + } + + /// Asynchronously close current connection + /// + /// Options: + /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) + /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) + /// * `callback` function to apply on complete + pub fn close_async( + self, + cancelable: Option, + priority: Option, + callback: impl FnOnce(Result<(), Error>) + 'static, + ) { + self.connection.close_async( + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + |result| { + callback(match result { + Ok(_) => Ok(()), + Err(_) => Err(Error::Close), + }) + }, + ); + } + + // Getters + + pub fn connection(&self) -> &SocketConnection { + &self.connection + } +} diff --git a/src/client/socket/connection/error.rs b/src/client/socket/connection/error.rs new file mode 100644 index 0000000..7405d6a --- /dev/null +++ b/src/client/socket/connection/error.rs @@ -0,0 +1,7 @@ +pub enum Error { + Close, + InputBufferOverflow, + InputBufferWrite, + InputStreamChunkRead, + OutputStreamWrite, +} diff --git a/src/client/socket/connection/input.rs b/src/client/socket/connection/input.rs new file mode 100644 index 0000000..16ba332 --- /dev/null +++ b/src/client/socket/connection/input.rs @@ -0,0 +1,155 @@ +pub mod buffer; +pub mod error; + +pub use buffer::Buffer; +pub use error::Error; + +use gio::{prelude::InputStreamExt, Cancellable, InputStream}; +use glib::Priority; + +pub const DEFAULT_READ_CHUNK: usize = 0x100; + +pub struct Input { + buffer: Buffer, + stream: InputStream, +} + +impl Input { + // Constructors + + /// Create new `Input` from `gio::InputStream` + /// + /// https://docs.gtk.org/gio/class.InputStream.html + pub fn new_from_stream(stream: InputStream) -> Self { + Self { + buffer: Buffer::new(), + stream, + } + } + + // Actions + + /// Synchronously read all bytes from `gio::InputStream` to `input::Buffer` + /// + /// Options: + /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html + /// * `chunk` max bytes to read per chunk (256 by default) + pub fn read_all( + mut self, + cancelable: Option, + chunk: Option, + ) -> Result { + loop { + // Continue bytes reading + match self.stream.read_bytes( + match chunk { + Some(value) => value, + None => DEFAULT_READ_CHUNK, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + ) { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return Ok(self.buffer); + } + + // Save chunk to buffer + match self.buffer.push(bytes) { + Ok(_) => continue, + Err(buffer::Error::Overflow) => return Err(Error::BufferOverflow), + Err(_) => return Err(Error::BufferWrite), + }; + } + Err(_) => return Err(Error::StreamChunkRead), + }; + } + } + + /// Asynchronously read all bytes from `gio::InputStream` to `input::Buffer`, + /// + /// applies `callback` function on last byte reading complete. + /// + /// Options: + /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) + /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) + /// * `chunk` optional max bytes to read per chunk (`DEFAULT_READ_CHUNK` by default) + /// * `callback` user function to apply on async iteration complete or `None` to skip + pub fn read_all_async( + mut self, + cancelable: Option, + priority: Option, + chunk: Option, + callback: impl FnOnce(Result) + 'static, + ) { + // Continue bytes reading + self.stream.clone().read_bytes_async( + match chunk { + Some(value) => value, + None => DEFAULT_READ_CHUNK, + }, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + move |result| { + match result { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return callback(Ok(self.buffer)); + } + + // Save chunk to buffer + match self.buffer.push(bytes) { + Err(buffer::Error::Overflow) => { + return callback(Err(Error::BufferOverflow)) + } + + // Other errors related to write issues @TODO test + Err(_) => return callback(Err(Error::BufferWrite)), + + // Async function, nothing to return yet + _ => (), + }; + + // Continue bytes reading... + self.read_all_async(cancelable, priority, chunk, callback); + } + Err(_) => callback(Err(Error::StreamChunkRead)), + } + }, + ); + } + + // Setters + + pub fn set_buffer(&mut self, buffer: Buffer) { + self.buffer = buffer; + } + + pub fn set_stream(&mut self, stream: InputStream) { + self.stream = stream; + } + + // Getters + + /// Get reference to `Buffer` + pub fn buffer(&self) -> &Buffer { + &self.buffer + } + + /// Get reference to `gio::InputStream` + pub fn stream(&self) -> &InputStream { + &self.stream + } +} diff --git a/src/client/socket/connection/input/buffer.rs b/src/client/socket/connection/input/buffer.rs new file mode 100644 index 0000000..b9264c7 --- /dev/null +++ b/src/client/socket/connection/input/buffer.rs @@ -0,0 +1,87 @@ +pub mod error; +pub use error::Error; + +use glib::Bytes; + +pub const DEFAULT_CAPACITY: usize = 0x400; +pub const DEFAULT_MAX_SIZE: usize = 0xfffff; + +pub struct Buffer { + bytes: Vec, + max_size: usize, +} + +impl Buffer { + // Constructors + + /// Create new dynamically allocated `Buffer` with default `capacity` and `max_size` limit + pub fn new() -> Self { + Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) + } + + /// Create new dynamically allocated `Buffer` with options + /// + /// Options: + /// * `capacity` initial bytes request to reduce extra memory overwrites (1024 by default) + /// * `max_size` max bytes to prevent memory overflow (1M by default) + pub fn new_with_options(capacity: Option, max_size: Option) -> Self { + Self { + bytes: Vec::with_capacity(match capacity { + Some(value) => value, + None => DEFAULT_CAPACITY, + }), + max_size: match max_size { + Some(value) => value, + None => DEFAULT_MAX_SIZE, + }, + } + } + + // Setters + + /// Set new `Buffer.max_size` value to prevent memory overflow + /// + /// Use `DEFAULT_MAX_SIZE` if `None` given. + pub fn set_max_size(&mut self, value: Option) { + self.max_size = match value { + Some(size) => size, + None => DEFAULT_MAX_SIZE, + } + } + + // Actions + + /// Push `glib::Bytes` to `Buffer.bytes` + /// + /// Return `Error::Overflow` on `Buffer.max_size` reached. + pub fn push(&mut self, bytes: Bytes) -> Result { + // Calculate new size value + let total = self.bytes.len() + bytes.len(); + + // Validate overflow + if total > self.max_size { + return Err(Error::Overflow); + } + + // Success + self.bytes.push(bytes); + + Ok(total) + } + + // Getters + + /// Get reference to bytes collected + pub fn bytes(&self) -> &Vec { + &self.bytes + } + + /// Return copy of bytes as UTF-8 vector + pub fn to_utf8(&self) -> Vec { + self.bytes + .iter() + .flat_map(|byte| byte.iter()) + .cloned() + .collect() + } +} diff --git a/src/client/socket/connection/input/buffer/error.rs b/src/client/socket/connection/input/buffer/error.rs new file mode 100644 index 0000000..68dd893 --- /dev/null +++ b/src/client/socket/connection/input/buffer/error.rs @@ -0,0 +1,4 @@ +pub enum Error { + Overflow, + StreamChunkRead, +} diff --git a/src/client/socket/connection/input/error.rs b/src/client/socket/connection/input/error.rs new file mode 100644 index 0000000..1c383fb --- /dev/null +++ b/src/client/socket/connection/input/error.rs @@ -0,0 +1,5 @@ +pub enum Error { + BufferOverflow, + BufferWrite, + StreamChunkRead, +} diff --git a/src/client/socket/connection/output.rs b/src/client/socket/connection/output.rs new file mode 100644 index 0000000..d6f1821 --- /dev/null +++ b/src/client/socket/connection/output.rs @@ -0,0 +1,71 @@ +pub mod error; + +pub use error::Error; + +use gio::{prelude::OutputStreamExt, Cancellable, OutputStream}; +use glib::{Bytes, Priority}; + +pub struct Output { + stream: OutputStream, +} + +impl Output { + // Constructors + + /// Create new `Output` from `gio::OutputStream` + /// + /// https://docs.gtk.org/gio/class.OutputStream.html + pub fn new_from_stream(stream: OutputStream) -> Self { + Self { stream } + } + + // Actions + + /// Asynchronously write all bytes to `gio::OutputStream`, + /// + /// applies `callback` function on last byte sent. + /// + /// Options: + /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) + /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) + /// * `callback` user function to apply on complete + pub fn write_async( + &self, + bytes: &Bytes, + cancelable: Option, + priority: Option, + callback: impl FnOnce(Result) + 'static, + ) { + self.stream.write_bytes_async( + bytes, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + move |result| { + callback(match result { + Ok(size) => Ok(size), + Err(_) => Err(Error::StreamWrite), + }) + }, + ); + } + + // Setters + + pub fn set_stream(&mut self, stream: OutputStream) { + self.stream = stream; + } + + // Getters + + /// Get reference to `gio::OutputStream` + pub fn stream(&self) -> &OutputStream { + &self.stream + } +} diff --git a/src/client/socket/connection/output/error.rs b/src/client/socket/connection/output/error.rs new file mode 100644 index 0000000..27547a1 --- /dev/null +++ b/src/client/socket/connection/output/error.rs @@ -0,0 +1,3 @@ +pub enum Error { + StreamWrite, +} diff --git a/src/client/socket/error.rs b/src/client/socket/error.rs new file mode 100644 index 0000000..1863964 --- /dev/null +++ b/src/client/socket/error.rs @@ -0,0 +1,3 @@ +pub enum Error { + Connection, +} From a8230bed37d516f892245d654bb3425942bef72d Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 15:01:17 +0300 Subject: [PATCH 010/392] update api --- src/client.rs | 59 ++++---- src/client/connection.rs | 5 - src/client/connection/input.rs | 156 -------------------- src/client/connection/input/buffer.rs | 87 ----------- src/client/connection/input/buffer/error.rs | 5 - src/client/connection/input/error.rs | 6 - src/client/error.rs | 3 +- src/client/socket.rs | 19 ++- src/client/socket/connection.rs | 76 +++++----- 9 files changed, 91 insertions(+), 325 deletions(-) delete mode 100644 src/client/connection.rs delete mode 100644 src/client/connection/input.rs delete mode 100644 src/client/connection/input/buffer.rs delete mode 100644 src/client/connection/input/buffer/error.rs delete mode 100644 src/client/connection/input/error.rs diff --git a/src/client.rs b/src/client.rs index 3158ff2..2690c91 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,35 +7,40 @@ pub use error::Error; pub use response::Response; pub use socket::Socket; -use gio::Cancellable; -use glib::{Priority, Uri}; +use glib::Uri; -/// High-level API to make async request, get `Response` and close connection. -pub fn request_async( +/// High-level API to make single async request +/// +/// 1. open new socket connection for [Uri](https://docs.gtk.org/glib/struct.Uri.html) +/// 2. send request +/// 3. read response +/// 4. close connection +/// 5. return `Result` to `callback` function +pub fn simple_socket_request_async( uri: Uri, - cancelable: Option, - priority: Option, callback: impl FnOnce(Result) + 'static, ) { - // Create new socket connection - Socket::new().connect_async( - uri.clone(), - cancelable.clone(), - move |connect| match connect { - Ok(connection) => { - connection.request_async(uri, cancelable, priority, None, |request| { - callback(match request { - Ok(buffer) => match Response::from_utf8(&buffer) { - Ok(response) => Ok(response), - Err(_) => Err(Error::Response), - }, - Err(_) => Err(Error::Request), - }); - - //connection.close_async(cancelable, priority, |_| {}); // @TODO - }) - } - Err(_) => callback(Err(Error::Connection)), - }, - ); + Socket::new().connect_async(uri.clone(), None, move |connect| match connect { + Ok(connection) => { + connection.request_async(uri, None, None, None, move |connection, response| { + connection.close_async( + None, + None, + Some(|close| { + callback(match close { + Ok(_) => match response { + Ok(buffer) => match Response::from_utf8(&buffer) { + Ok(response) => Ok(response), + Err(_) => Err(Error::Response), + }, + Err(_) => Err(Error::Request), + }, + Err(_) => Err(Error::Close), + }) + }), + ); + }) + } + Err(_) => callback(Err(Error::Connection)), + }); } diff --git a/src/client/connection.rs b/src/client/connection.rs deleted file mode 100644 index 7c2e44f..0000000 --- a/src/client/connection.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod input; - -pub use input::Input; - -// @TODO diff --git a/src/client/connection/input.rs b/src/client/connection/input.rs deleted file mode 100644 index 288d4e1..0000000 --- a/src/client/connection/input.rs +++ /dev/null @@ -1,156 +0,0 @@ -pub mod buffer; -pub mod error; - -pub use buffer::Buffer; -pub use error::Error; - -use gio::{prelude::InputStreamExt, Cancellable, InputStream}; -use glib::Priority; - -// Defaults -pub const DEFAULT_READ_CHUNK: usize = 0x100; - -pub struct Input { - buffer: Buffer, - stream: InputStream, -} - -impl Input { - // Constructors - - /// Create new `Input` from `gio::InputStream` - /// - /// https://docs.gtk.org/gio/class.InputStream.html - pub fn new_from_stream(stream: InputStream) -> Self { - Self { - buffer: Buffer::new(), - stream, - } - } - - // Actions - - /// Synchronously read all bytes from `gio::InputStream` to `input::Buffer` - /// - /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html - /// * `chunk` max bytes to read per chunk (256 by default) - pub fn read_all( - mut self, - cancelable: Option, - chunk: Option, - ) -> Result { - loop { - // Continue bytes reading - match self.stream.read_bytes( - match chunk { - Some(value) => value, - None => DEFAULT_READ_CHUNK, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - ) { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return Ok(self.buffer); - } - - // Save chunk to buffer - match self.buffer.push(bytes) { - Ok(_) => continue, - Err(buffer::Error::Overflow) => return Err(Error::BufferOverflow), - Err(_) => return Err(Error::BufferWrite), - }; - } - Err(_) => return Err(Error::StreamChunkRead), - }; - } - } - - /// Asynchronously read all bytes from `gio::InputStream` to `input::Buffer`, - /// - /// applies `callback` function on last byte reading complete. - /// - /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) - /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) - /// * `chunk` optional max bytes to read per chunk (`DEFAULT_READ_CHUNK` by default) - /// * `callback` user function to apply on async iteration complete - pub fn read_all_async( - mut self, - cancelable: Option, - priority: Option, - chunk: Option, - callback: impl FnOnce(Result) + 'static, - ) { - // Continue bytes reading - self.stream.clone().read_bytes_async( - match chunk { - Some(value) => value, - None => DEFAULT_READ_CHUNK, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - move |result| { - match result { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return callback(Ok(self.buffer)); - } - - // Save chunk to buffer - match self.buffer.push(bytes) { - Err(buffer::Error::Overflow) => { - return callback(Err(Error::BufferOverflow)) - } - - // Other errors related to write issues @TODO test - Err(_) => return callback(Err(Error::BufferWrite)), - - // Async function, nothing to return yet - _ => (), - }; - - // Continue bytes reading... - self.read_all_async(cancelable, priority, chunk, callback); - } - Err(_) => callback(Err(Error::StreamChunkReadAsync)), - } - }, - ); - } - - // Setters - - pub fn set_buffer(&mut self, buffer: Buffer) { - self.buffer = buffer; - } - - pub fn set_stream(&mut self, stream: InputStream) { - self.stream = stream; - } - - // Getters - - /// Get reference to `Buffer` - pub fn buffer(&self) -> &Buffer { - &self.buffer - } - - /// Get reference to `gio::InputStream` - pub fn stream(&self) -> &InputStream { - &self.stream - } -} diff --git a/src/client/connection/input/buffer.rs b/src/client/connection/input/buffer.rs deleted file mode 100644 index b9264c7..0000000 --- a/src/client/connection/input/buffer.rs +++ /dev/null @@ -1,87 +0,0 @@ -pub mod error; -pub use error::Error; - -use glib::Bytes; - -pub const DEFAULT_CAPACITY: usize = 0x400; -pub const DEFAULT_MAX_SIZE: usize = 0xfffff; - -pub struct Buffer { - bytes: Vec, - max_size: usize, -} - -impl Buffer { - // Constructors - - /// Create new dynamically allocated `Buffer` with default `capacity` and `max_size` limit - pub fn new() -> Self { - Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) - } - - /// Create new dynamically allocated `Buffer` with options - /// - /// Options: - /// * `capacity` initial bytes request to reduce extra memory overwrites (1024 by default) - /// * `max_size` max bytes to prevent memory overflow (1M by default) - pub fn new_with_options(capacity: Option, max_size: Option) -> Self { - Self { - bytes: Vec::with_capacity(match capacity { - Some(value) => value, - None => DEFAULT_CAPACITY, - }), - max_size: match max_size { - Some(value) => value, - None => DEFAULT_MAX_SIZE, - }, - } - } - - // Setters - - /// Set new `Buffer.max_size` value to prevent memory overflow - /// - /// Use `DEFAULT_MAX_SIZE` if `None` given. - pub fn set_max_size(&mut self, value: Option) { - self.max_size = match value { - Some(size) => size, - None => DEFAULT_MAX_SIZE, - } - } - - // Actions - - /// Push `glib::Bytes` to `Buffer.bytes` - /// - /// Return `Error::Overflow` on `Buffer.max_size` reached. - pub fn push(&mut self, bytes: Bytes) -> Result { - // Calculate new size value - let total = self.bytes.len() + bytes.len(); - - // Validate overflow - if total > self.max_size { - return Err(Error::Overflow); - } - - // Success - self.bytes.push(bytes); - - Ok(total) - } - - // Getters - - /// Get reference to bytes collected - pub fn bytes(&self) -> &Vec { - &self.bytes - } - - /// Return copy of bytes as UTF-8 vector - pub fn to_utf8(&self) -> Vec { - self.bytes - .iter() - .flat_map(|byte| byte.iter()) - .cloned() - .collect() - } -} diff --git a/src/client/connection/input/buffer/error.rs b/src/client/connection/input/buffer/error.rs deleted file mode 100644 index bab66ff..0000000 --- a/src/client/connection/input/buffer/error.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub enum Error { - Overflow, - StreamChunkRead, - StreamChunkReadAsync, -} diff --git a/src/client/connection/input/error.rs b/src/client/connection/input/error.rs deleted file mode 100644 index 4bcff86..0000000 --- a/src/client/connection/input/error.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub enum Error { - BufferOverflow, - BufferWrite, - StreamChunkRead, - StreamChunkReadAsync, -} diff --git a/src/client/error.rs b/src/client/error.rs index d14b8f1..10cda29 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -1,5 +1,6 @@ pub enum Error { + Close, Connection, - Response, Request, + Response, } diff --git a/src/client/socket.rs b/src/client/socket.rs index 1146cf1..6729e18 100644 --- a/src/client/socket.rs +++ b/src/client/socket.rs @@ -13,6 +13,7 @@ use glib::Uri; pub struct Socket { client: SocketClient, + default_port: u16, } impl Socket { @@ -26,7 +27,10 @@ impl Socket { client.set_tls_validation_flags(TlsCertificateFlags::INSECURE); client.set_tls(true); - Self { client } + Self { + client, + default_port: DEFAULT_PORT, + } } // Actions @@ -38,7 +42,7 @@ impl Socket { ) { self.client.connect_to_uri_async( uri.to_str().as_str(), - DEFAULT_PORT, + self.default_port, match cancelable.clone() { Some(value) => Some(value), None => None::, @@ -53,9 +57,18 @@ impl Socket { ); } + // Setters + + /// Set default port for socket connections (1965 by default) + pub fn set_default_port(&mut self, default_port: u16) { + self.default_port = default_port; + } + // Getters - /// Return ref to `gio::SocketClient` GObject + /// Get reference to `gio::SocketClient` + /// + /// https://docs.gtk.org/gio/class.SocketClient.html pub fn client(&self) -> &SocketClient { self.client.as_ref() } diff --git a/src/client/socket/connection.rs b/src/client/socket/connection.rs index b9b6e2f..bf187ab 100644 --- a/src/client/socket/connection.rs +++ b/src/client/socket/connection.rs @@ -16,61 +16,62 @@ pub struct Connection { impl Connection { // Constructors + /// Create new `Self` from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) pub fn new_from(connection: SocketConnection) -> Self { Self { connection } } // Actions - /// MIddle-level API to make single async socket request for current connection: + /// Middle-level API to make async socket request for current connection: /// - /// 1. send `Uri` request to the ouput stream; - /// 2. write entire input stream into the new `Vec` buffer on success; - /// 3. close current connection if callback function does not prevent that action by return. + /// 1. send request for [Uri](https://docs.gtk.org/glib/struct.Uri.html) + /// to the ouput [OutputStream](https://docs.gtk.org/gio/class.OutputStream.html); + /// 2. write entire [InputStream](https://docs.gtk.org/gio/class.InputStream.html) + /// into `Vec` buffer on success; + /// 3. return taken `Self` with `Result(Vec, Error)` on complete. pub fn request_async( self, uri: Uri, cancelable: Option, priority: Option, chunk: Option, - callback: impl FnOnce(Result, Error>) + 'static, + callback: impl FnOnce(Self, Result, Error>) + 'static, ) { - // Send request Output::new_from_stream(self.connection.output_stream()).write_async( &Bytes::from(gformat!("{}\r\n", uri.to_str()).as_bytes()), cancelable.clone(), priority, move |output| match output { Ok(_) => { - // Read response Input::new_from_stream(self.connection.input_stream()).read_all_async( cancelable.clone(), priority, chunk, move |input| { - // Apply callback function - callback(match input { - Ok(buffer) => Ok(buffer.to_utf8()), - Err(error) => Err(match error { - input::Error::BufferOverflow => Error::InputBufferOverflow, - input::Error::BufferWrite => Error::InputBufferWrite, - input::Error::StreamChunkRead => Error::InputStreamChunkRead, - }), - }); - - // Close connection if callback act does not prevent that - self.close_async(cancelable, priority, |_| {}); // @TODO + callback( + self, + match input { + Ok(buffer) => Ok(buffer.to_utf8()), + Err(error) => Err(match error { + input::Error::BufferOverflow => Error::InputBufferOverflow, + input::Error::BufferWrite => Error::InputBufferWrite, + input::Error::StreamChunkRead => { + Error::InputStreamChunkRead + } + }), + }, + ); }, ); } Err(error) => { - // Apply callback function - callback(Err(match error { - output::Error::StreamWrite => Error::OutputStreamWrite, - })); - - // Close connection if callback act does not prevent that - self.close_async(cancelable, priority, |_| {}); // @TODO + callback( + self, + Err(match error { + output::Error::StreamWrite => Error::OutputStreamWrite, + }), + ); } }, ); @@ -79,14 +80,14 @@ impl Connection { /// Asynchronously close current connection /// /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) - /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) - /// * `callback` function to apply on complete + /// * `cancellable` see [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) (`None::<&Cancellable>` by default) + /// * `priority` [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default + /// * `callback` optional function to apply on complete or `None` to skip pub fn close_async( - self, + &self, cancelable: Option, priority: Option, - callback: impl FnOnce(Result<(), Error>) + 'static, + callback: Option) + 'static>, ) { self.connection.close_async( match priority { @@ -99,16 +100,21 @@ impl Connection { } .as_ref(), |result| { - callback(match result { - Ok(_) => Ok(()), - Err(_) => Err(Error::Close), - }) + if let Some(call) = callback { + call(match result { + Ok(_) => Ok(()), + Err(_) => Err(Error::Close), + }) + } }, ); } // Getters + /// Get reference to `gio::SocketConnection` + /// + /// https://docs.gtk.org/gio/class.SocketConnection.html pub fn connection(&self) -> &SocketConnection { &self.connection } From fd96406daefeb21a35181ff7c020847705197dc3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 15:46:23 +0300 Subject: [PATCH 011/392] remove extra line --- src/client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 2690c91..0753145 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,6 @@ pub mod response; pub mod socket; pub use error::Error; - pub use response::Response; pub use socket::Socket; From afc030420bd6ead175587342ed2fd2b5231e1111 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:13:57 +0300 Subject: [PATCH 012/392] draft test --- tests/client.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/client.rs diff --git a/tests/client.rs b/tests/client.rs new file mode 100644 index 0000000..063145e --- /dev/null +++ b/tests/client.rs @@ -0,0 +1,21 @@ +use glib::{Uri, UriFlags}; + +#[test] +fn simple_socket_request_async() { + // Parse URI + match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { + // Begin async request + Ok(uri) => ggemini::client::simple_socket_request_async(uri, |response| match response { + // Process response + Ok(response) => { + // Expect success status + assert!(match response.header().status() { + Some(ggemini::client::response::header::Status::Success) => true, + _ => false, + }) + } + Err(_) => assert!(false), + }), + Err(_) => assert!(false), + } +} // @TODO async From 052003b6bec69ac5c0f56dbeb8f0e44732e1e996 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:14:53 +0300 Subject: [PATCH 013/392] update readme --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/README.md b/README.md index c450e5c..56f83d5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,63 @@ Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > +## Install + +``` +cargo add ggemini +``` + +## Usage + +## `client` + + +#### `client::simple_socket_request_async` + +High-level API to make async socket request and auto-close connection on complete. + +Return [Response](#client::response::Response) on success or [Error](#client::Error) enum on failure. + +``` rust +use glib::{Uri, UriFlags}; + +match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { + // Begin async request + Ok(uri) => ggemini::client::simple_socket_request_async(uri, |response| match response { + // Process response + Ok(response) => { + // Expect success status + assert!(match response.header().status() { + Some(ggemini::client::response::header::Status::Success) => true, + _ => false, + }) + } + Err(_) => assert!(false), + }), + Err(_) => assert!(false), +} +``` + +#### `client::Error` + +#### `client::response` +#### `client::response::Response` + +#### `client::response::header` +#### `client::response::header::meta` +#### `client::response::header::mime` +#### `client::response::header::status` +#### `client::response::header::language` +#### `client::response::header::charset` + +#### `client::response::body` + +#### `client::socket` +#### `client::socket::connection` +#### `client::socket::connection::input` +#### `client::socket::connection::input::buffer` +#### `client::socket::connection::output` + ## Integrations * [Yoda](https://github.com/YGGverse/Yoda) - Browser for Gemini Protocol From ff42d022a4dd8df79540585ab9fd9b4b36e77678 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:16:02 +0300 Subject: [PATCH 014/392] fix anchors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56f83d5..d8558d8 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ cargo add ggemini High-level API to make async socket request and auto-close connection on complete. -Return [Response](#client::response::Response) on success or [Error](#client::Error) enum on failure. +Return [Response](#client_response_Response) on success or [Error](#client_Error) enum on failure. ``` rust use glib::{Uri, UriFlags}; From 51685f18b7cfcf191b4e63b5d20c0f316331dd39 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:17:02 +0300 Subject: [PATCH 015/392] fix anchors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8558d8..a2cbed0 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ cargo add ggemini High-level API to make async socket request and auto-close connection on complete. -Return [Response](#client_response_Response) on success or [Error](#client_Error) enum on failure. +Return [Response](#clientresponseresponse) on success or [Error](#clienterror) enum on failure. ``` rust use glib::{Uri, UriFlags}; From 62bbb1a92531353cc48d953a6ddc4dc7a5eec52a Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:17:53 +0300 Subject: [PATCH 016/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2cbed0..1db9c4e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ggemini -Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/) +[Glib](https://docs.gtk.org/glib/)-oriented client for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! From 64eb173319739d3bbfdf990d69eb24740e317e65 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:18:19 +0300 Subject: [PATCH 017/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1db9c4e..a2cbed0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ggemini -[Glib](https://docs.gtk.org/glib/)-oriented client for [Gemini protocol](https://geminiprotocol.net/) +Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! From c9c8b39513f5010b6fb19d7905a9fc1954774803 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:19:11 +0300 Subject: [PATCH 018/392] update variable name --- README.md | 2 +- tests/client.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a2cbed0..f46e581 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ use glib::{Uri, UriFlags}; match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { // Begin async request - Ok(uri) => ggemini::client::simple_socket_request_async(uri, |response| match response { + Ok(uri) => ggemini::client::simple_socket_request_async(uri, |result| match result { // Process response Ok(response) => { // Expect success status diff --git a/tests/client.rs b/tests/client.rs index 063145e..d3921b8 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -5,7 +5,7 @@ fn simple_socket_request_async() { // Parse URI match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { // Begin async request - Ok(uri) => ggemini::client::simple_socket_request_async(uri, |response| match response { + Ok(uri) => ggemini::client::simple_socket_request_async(uri, |result| match result { // Process response Ok(response) => { // Expect success status From bfc52606c30169e0ab6d1c487bbb1a42b0e4d25a Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:21:56 +0300 Subject: [PATCH 019/392] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f46e581..b753b78 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Return [Response](#clientresponseresponse) on success or [Error](#clienterror) e ``` rust use glib::{Uri, UriFlags}; +// Parse URL string to valid URI match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { // Begin async request Ok(uri) => ggemini::client::simple_socket_request_async(uri, |result| match result { From 16af9499b51a3d5c34971d0410c4798a2758575b Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:34:10 +0300 Subject: [PATCH 020/392] rename method --- README.md | 4 ++-- src/client.rs | 2 +- tests/client.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b753b78..e342450 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ cargo add ggemini ## `client` -#### `client::simple_socket_request_async` +#### `client::single_socket_request_async` High-level API to make async socket request and auto-close connection on complete. @@ -29,7 +29,7 @@ use glib::{Uri, UriFlags}; // Parse URL string to valid URI match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { // Begin async request - Ok(uri) => ggemini::client::simple_socket_request_async(uri, |result| match result { + Ok(uri) => ggemini::client::single_socket_request_async(uri, |result| match result { // Process response Ok(response) => { // Expect success status diff --git a/src/client.rs b/src/client.rs index 0753145..8b05a36 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,7 +15,7 @@ use glib::Uri; /// 3. read response /// 4. close connection /// 5. return `Result` to `callback` function -pub fn simple_socket_request_async( +pub fn single_socket_request_async( uri: Uri, callback: impl FnOnce(Result) + 'static, ) { diff --git a/tests/client.rs b/tests/client.rs index d3921b8..e98122c 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -1,11 +1,11 @@ use glib::{Uri, UriFlags}; #[test] -fn simple_socket_request_async() { +fn single_socket_request_async() { // Parse URI match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { // Begin async request - Ok(uri) => ggemini::client::simple_socket_request_async(uri, |result| match result { + Ok(uri) => ggemini::client::single_socket_request_async(uri, |result| match result { // Process response Ok(response) => { // Expect success status From 60999bfa97cc6e44023294a0f8855f91dc9db47f Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 16:45:00 +0300 Subject: [PATCH 021/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e342450..c7f9508 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Return [Response](#clientresponseresponse) on success or [Error](#clienterror) e ``` rust use glib::{Uri, UriFlags}; -// Parse URL string to valid URI +// Parse URL string to valid Glib URI object match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { // Begin async request Ok(uri) => ggemini::client::single_socket_request_async(uri, |result| match result { From 4bcbf501631a437a224ef7c3aedd08fdb8b478f3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 19:30:19 +0300 Subject: [PATCH 022/392] update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c7f9508..a6ded1b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,11 @@ match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { } ``` +Pay attention: + +* Response [Buffer](#clientsocketconnectioninputbufferBuffer) limited to default `capacity` (0x400) and `max_size` (0xfffff). If you want to change these values, use low-level API to setup connection manually. +* If you want to use [Cancelable](https://docs.gtk.org/gio/class.Cancellable.html) or async Priority values, take a look at [connection](#clientsocketconnection) methods. + #### `client::Error` #### `client::response` @@ -62,6 +67,7 @@ match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { #### `client::socket::connection` #### `client::socket::connection::input` #### `client::socket::connection::input::buffer` +#### `client::socket::connection::input::buffer::Buffer` #### `client::socket::connection::output` ## Integrations From 6819de52763df7e443329ffac2cd3a3e6adc5c3c Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 19:30:51 +0300 Subject: [PATCH 023/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6ded1b..65ea3df 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { Pay attention: * Response [Buffer](#clientsocketconnectioninputbufferBuffer) limited to default `capacity` (0x400) and `max_size` (0xfffff). If you want to change these values, use low-level API to setup connection manually. -* If you want to use [Cancelable](https://docs.gtk.org/gio/class.Cancellable.html) or async Priority values, take a look at [connection](#clientsocketconnection) methods. +* To use [Cancelable](https://docs.gtk.org/gio/class.Cancellable.html) or async Priority values, take a look at [connection](#clientsocketconnection) methods. #### `client::Error` From 1f25565a7938176aa963ae7391e53b9381b3a832 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 19:31:59 +0300 Subject: [PATCH 024/392] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65ea3df..008f41e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ cargo add ggemini High-level API to make async socket request and auto-close connection on complete. -Return [Response](#clientresponseresponse) on success or [Error](#clienterror) enum on failure. +Return [Response](#clientresponseresponse) on success or [Error](#clienterror) enum on failure ``` rust use glib::{Uri, UriFlags}; @@ -44,7 +44,7 @@ match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { } ``` -Pay attention: +**Pay attention:** * Response [Buffer](#clientsocketconnectioninputbufferBuffer) limited to default `capacity` (0x400) and `max_size` (0xfffff). If you want to change these values, use low-level API to setup connection manually. * To use [Cancelable](https://docs.gtk.org/gio/class.Cancellable.html) or async Priority values, take a look at [connection](#clientsocketconnection) methods. From 7cb817a173527837343fd7f03c7fe354aea06ff9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 19:34:11 +0300 Subject: [PATCH 025/392] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 008f41e..dc1c027 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ cargo add ggemini #### `client::single_socket_request_async` -High-level API to make async socket request and auto-close connection on complete. +High-level API to make async socket request, auto-close connection on complete. Return [Response](#clientresponseresponse) on success or [Error](#clienterror) enum on failure @@ -46,7 +46,7 @@ match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { **Pay attention:** -* Response [Buffer](#clientsocketconnectioninputbufferBuffer) limited to default `capacity` (0x400) and `max_size` (0xfffff). If you want to change these values, use low-level API to setup connection manually. +* Response [Buffer](#clientsocketconnectioninputbufferBuffer) limited to default `capacity` (`0x400`) and `max_size` (`0xfffff`). If you want to change these values, use low-level API to setup connection manually. * To use [Cancelable](https://docs.gtk.org/gio/class.Cancellable.html) or async Priority values, take a look at [connection](#clientsocketconnection) methods. #### `client::Error` From 25bcb58fc4234e97c90f700aa726d4fde0ab46a1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 20:53:57 +0300 Subject: [PATCH 026/392] update comment, change reference method --- src/client/socket.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/socket.rs b/src/client/socket.rs index 6729e18..4e541f7 100644 --- a/src/client/socket.rs +++ b/src/client/socket.rs @@ -59,7 +59,7 @@ impl Socket { // Setters - /// Set default port for socket connections (1965 by default) + /// Change default port for this socket connections (`1965` by default) pub fn set_default_port(&mut self, default_port: u16) { self.default_port = default_port; } @@ -70,6 +70,6 @@ impl Socket { /// /// https://docs.gtk.org/gio/class.SocketClient.html pub fn client(&self) -> &SocketClient { - self.client.as_ref() + &self.client } } From 32c238da4d27a8241ba7144f26c7026db6e7cfe5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 20:55:30 +0300 Subject: [PATCH 027/392] return entire self taken --- src/client/socket/connection.rs | 2 +- src/client/socket/connection/input.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/client/socket/connection.rs b/src/client/socket/connection.rs index bf187ab..28bdf42 100644 --- a/src/client/socket/connection.rs +++ b/src/client/socket/connection.rs @@ -52,7 +52,7 @@ impl Connection { callback( self, match input { - Ok(buffer) => Ok(buffer.to_utf8()), + Ok(this) => Ok(this.buffer().to_utf8()), Err(error) => Err(match error { input::Error::BufferOverflow => Error::InputBufferOverflow, input::Error::BufferWrite => Error::InputBufferWrite, diff --git a/src/client/socket/connection/input.rs b/src/client/socket/connection/input.rs index 16ba332..8938129 100644 --- a/src/client/socket/connection/input.rs +++ b/src/client/socket/connection/input.rs @@ -31,6 +31,8 @@ impl Input { /// Synchronously read all bytes from `gio::InputStream` to `input::Buffer` /// + /// Return `Self` with `buffer` updated on success + /// /// Options: /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html /// * `chunk` max bytes to read per chunk (256 by default) @@ -38,7 +40,7 @@ impl Input { mut self, cancelable: Option, chunk: Option, - ) -> Result { + ) -> Result { loop { // Continue bytes reading match self.stream.read_bytes( @@ -55,7 +57,7 @@ impl Input { Ok(bytes) => { // No bytes were read, end of stream if bytes.len() == 0 { - return Ok(self.buffer); + return Ok(self); } // Save chunk to buffer @@ -70,9 +72,10 @@ impl Input { } } - /// Asynchronously read all bytes from `gio::InputStream` to `input::Buffer`, + /// Asynchronously read all bytes from `gio::InputStream` to `input::Buffer` /// - /// applies `callback` function on last byte reading complete. + /// * applies `callback` function on last byte reading complete; + /// * return `Self` with `buffer` updated on success /// /// Options: /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) @@ -84,7 +87,7 @@ impl Input { cancelable: Option, priority: Option, chunk: Option, - callback: impl FnOnce(Result) + 'static, + callback: impl FnOnce(Result) + 'static, ) { // Continue bytes reading self.stream.clone().read_bytes_async( @@ -106,7 +109,7 @@ impl Input { Ok(bytes) => { // No bytes were read, end of stream if bytes.len() == 0 { - return callback(Ok(self.buffer)); + return callback(Ok(self)); } // Save chunk to buffer From 8a5f1e2a57b8b288316aaf1b3bd492c9db228ee3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 25 Oct 2024 21:01:06 +0300 Subject: [PATCH 028/392] return self anyway --- src/client/socket/connection.rs | 4 ++-- src/client/socket/connection/input.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/socket/connection.rs b/src/client/socket/connection.rs index 28bdf42..255a803 100644 --- a/src/client/socket/connection.rs +++ b/src/client/socket/connection.rs @@ -48,11 +48,11 @@ impl Connection { cancelable.clone(), priority, chunk, - move |input| { + move |this, input| { callback( self, match input { - Ok(this) => Ok(this.buffer().to_utf8()), + Ok(()) => Ok(this.buffer().to_utf8()), Err(error) => Err(match error { input::Error::BufferOverflow => Error::InputBufferOverflow, input::Error::BufferWrite => Error::InputBufferWrite, diff --git a/src/client/socket/connection/input.rs b/src/client/socket/connection/input.rs index 8938129..1c062ee 100644 --- a/src/client/socket/connection/input.rs +++ b/src/client/socket/connection/input.rs @@ -87,7 +87,7 @@ impl Input { cancelable: Option, priority: Option, chunk: Option, - callback: impl FnOnce(Result) + 'static, + callback: impl FnOnce(Self, Result<(), Error>) + 'static, ) { // Continue bytes reading self.stream.clone().read_bytes_async( @@ -109,17 +109,17 @@ impl Input { Ok(bytes) => { // No bytes were read, end of stream if bytes.len() == 0 { - return callback(Ok(self)); + return callback(self, Ok(())); } // Save chunk to buffer match self.buffer.push(bytes) { Err(buffer::Error::Overflow) => { - return callback(Err(Error::BufferOverflow)) + return callback(self, Err(Error::BufferOverflow)) } // Other errors related to write issues @TODO test - Err(_) => return callback(Err(Error::BufferWrite)), + Err(_) => return callback(self, Err(Error::BufferWrite)), // Async function, nothing to return yet _ => (), @@ -128,7 +128,7 @@ impl Input { // Continue bytes reading... self.read_all_async(cancelable, priority, chunk, callback); } - Err(_) => callback(Err(Error::StreamChunkRead)), + Err(_) => callback(self, Err(Error::StreamChunkRead)), } }, ); From 3cde80b6a88ed61637c7e923e4baf4212f746f8a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:22:26 +0300 Subject: [PATCH 029/392] draft new api version --- README.md | 65 ++----- src/client.rs | 42 +---- src/client/buffer.rs | 164 ++++++++++++++++++ .../connection/input => }/buffer/error.rs | 2 +- src/client/response.rs | 16 +- src/client/response/body.rs | 12 +- src/client/response/header.rs | 60 ++++--- src/client/response/header/error.rs | 3 +- src/client/response/header/meta.rs | 13 +- src/client/response/header/mime.rs | 105 ++++++----- src/client/response/header/mime/error.rs | 4 + src/client/response/header/status.rs | 32 ++-- src/client/socket.rs | 75 -------- src/client/socket/connection.rs | 121 ------------- src/client/socket/connection/error.rs | 7 - src/client/socket/connection/input.rs | 158 ----------------- src/client/socket/connection/input/buffer.rs | 87 ---------- src/client/socket/connection/input/error.rs | 5 - src/client/socket/connection/output.rs | 71 -------- src/client/socket/connection/output/error.rs | 3 - src/client/socket/error.rs | 3 - tests/client.rs | 22 +-- 22 files changed, 323 insertions(+), 747 deletions(-) create mode 100644 src/client/buffer.rs rename src/client/{socket/connection/input => }/buffer/error.rs (61%) create mode 100644 src/client/response/header/mime/error.rs delete mode 100644 src/client/socket.rs delete mode 100644 src/client/socket/connection.rs delete mode 100644 src/client/socket/connection/error.rs delete mode 100644 src/client/socket/connection/input.rs delete mode 100644 src/client/socket/connection/input/buffer.rs delete mode 100644 src/client/socket/connection/input/error.rs delete mode 100644 src/client/socket/connection/output.rs delete mode 100644 src/client/socket/connection/output/error.rs delete mode 100644 src/client/socket/error.rs diff --git a/README.md b/README.md index dc1c027..ebc4727 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # ggemini -Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/) +Glib-oriented network library for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! > +This library initially created as extension for [Yoda Browser](https://github.com/YGGverse/Yoda), +but also could be useful for any other integration as depends of +[glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only. + ## Install ``` @@ -14,65 +18,22 @@ cargo add ggemini ## Usage -## `client` +### `client` +[Gio](https://docs.gtk.org/gio/) API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), +so this Client just bit extends some features for Gemini Protocol. -#### `client::single_socket_request_async` - -High-level API to make async socket request, auto-close connection on complete. - -Return [Response](#clientresponseresponse) on success or [Error](#clienterror) enum on failure - -``` rust -use glib::{Uri, UriFlags}; - -// Parse URL string to valid Glib URI object -match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { - // Begin async request - Ok(uri) => ggemini::client::single_socket_request_async(uri, |result| match result { - // Process response - Ok(response) => { - // Expect success status - assert!(match response.header().status() { - Some(ggemini::client::response::header::Status::Success) => true, - _ => false, - }) - } - Err(_) => assert!(false), - }), - Err(_) => assert!(false), -} -``` - -**Pay attention:** - -* Response [Buffer](#clientsocketconnectioninputbufferBuffer) limited to default `capacity` (`0x400`) and `max_size` (`0xfffff`). If you want to change these values, use low-level API to setup connection manually. -* To use [Cancelable](https://docs.gtk.org/gio/class.Cancellable.html) or async Priority values, take a look at [connection](#clientsocketconnection) methods. - -#### `client::Error` +#### `client::buffer` #### `client::response` + +Response parser for [InputStream](https://docs.gtk.org/gio/class.InputStream.html) + #### `client::response::Response` - #### `client::response::header` -#### `client::response::header::meta` -#### `client::response::header::mime` -#### `client::response::header::status` -#### `client::response::header::language` -#### `client::response::header::charset` - #### `client::response::body` -#### `client::socket` -#### `client::socket::connection` -#### `client::socket::connection::input` -#### `client::socket::connection::input::buffer` -#### `client::socket::connection::input::buffer::Buffer` -#### `client::socket::connection::output` - -## Integrations - -* [Yoda](https://github.com/YGGverse/Yoda) - Browser for Gemini Protocol +https://docs.gtk.org/glib/struct.Bytes.html ## See also diff --git a/src/client.rs b/src/client.rs index 8b05a36..2be3ea9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,45 +1,7 @@ +pub mod buffer; pub mod error; pub mod response; -pub mod socket; +pub use buffer::Buffer; pub use error::Error; pub use response::Response; -pub use socket::Socket; - -use glib::Uri; - -/// High-level API to make single async request -/// -/// 1. open new socket connection for [Uri](https://docs.gtk.org/glib/struct.Uri.html) -/// 2. send request -/// 3. read response -/// 4. close connection -/// 5. return `Result` to `callback` function -pub fn single_socket_request_async( - uri: Uri, - callback: impl FnOnce(Result) + 'static, -) { - Socket::new().connect_async(uri.clone(), None, move |connect| match connect { - Ok(connection) => { - connection.request_async(uri, None, None, None, move |connection, response| { - connection.close_async( - None, - None, - Some(|close| { - callback(match close { - Ok(_) => match response { - Ok(buffer) => match Response::from_utf8(&buffer) { - Ok(response) => Ok(response), - Err(_) => Err(Error::Response), - }, - Err(_) => Err(Error::Request), - }, - Err(_) => Err(Error::Close), - }) - }), - ); - }) - } - Err(_) => callback(Err(Error::Connection)), - }); -} diff --git a/src/client/buffer.rs b/src/client/buffer.rs new file mode 100644 index 0000000..f3e8d50 --- /dev/null +++ b/src/client/buffer.rs @@ -0,0 +1,164 @@ +pub mod error; +pub use error::Error; + +use gio::{ + prelude::{IOStreamExt, InputStreamExt}, + Cancellable, SocketConnection, +}; +use glib::{Bytes, Priority}; + +pub const DEFAULT_CAPACITY: usize = 0x400; +pub const DEFAULT_MAX_SIZE: usize = 0xfffff; + +/// Dynamically allocated [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) buffer +/// with configurable `capacity` and `max_size` limits +pub struct Buffer { + buffer: Vec, + max_size: usize, +} + +impl Buffer { + // Constructors + + /// Create new `Self` with default `capacity` and `max_size` preset + pub fn new() -> Self { + Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) + } + + /// Create new `Self` with options + /// + /// Options: + /// * `capacity` initial bytes request to reduce extra memory reallocation (`DEFAULT_CAPACITY` if `None`) + /// * `max_size` max bytes to prevent memory overflow by unknown stream source (`DEFAULT_MAX_SIZE` if `None`) + pub fn new_with_options(capacity: Option, max_size: Option) -> Self { + Self { + buffer: Vec::with_capacity(match capacity { + Some(value) => value, + None => DEFAULT_CAPACITY, + }), + max_size: match max_size { + Some(value) => value, + None => DEFAULT_MAX_SIZE, + }, + } + } + + // Intentable constructors + + /// Simplest way to create `Self` buffer from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + /// + /// Options: + /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from + /// * `callback` function to apply on all async operations complete, return `Result)>` + pub fn from_connection_async( + connection: SocketConnection, + callback: impl FnOnce(Result)>) + 'static, + ) { + Self::read_all_async(Self::new(), connection, None, None, None, callback); + } + + // Actions + + /// Asynchronously read all [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + /// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to `Self.buffer` + /// + /// Useful to grab entire stream without risk of memory overflow (according to `Self.max_size`), + /// reduce extra memory reallocations by `capacity` option. + /// + /// **Notes** + /// + /// We are using entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) reference + /// instead of [InputStream](https://docs.gtk.org/gio/class.InputStream.html) directly just to keep main connection alive in the async context + /// + /// **Options** + /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from + /// * `cancellable` - [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) or `None::<&Cancellable>` by default + /// * `priority` - [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default + /// * `chunk` optional bytes count to read per chunk (`0x100` by default) + /// * `callback` function to apply on all async operations complete, return `Result)>` + pub fn read_all_async( + mut self, + connection: SocketConnection, + cancelable: Option, + priority: Option, + chunk: Option, + callback: impl FnOnce(Result)>) + 'static, + ) { + connection.input_stream().read_bytes_async( + match chunk { + Some(value) => value, + None => 0x100, + }, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + move |result| match result { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return callback(Ok(self)); + } + + // Save chunk to buffer + if let Err(reason) = self.push(bytes) { + return callback(Err((reason, None))); + }; + + // Continue bytes read.. + self.read_all_async(connection, cancelable, priority, chunk, callback); + } + Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), + }, + ); + } + + /// Push [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to `Self.buffer` + /// + /// Return `Error::Overflow` on `max_size` reached + pub fn push(&mut self, bytes: Bytes) -> Result { + // Calculate new size value + let total = self.buffer.len() + bytes.len(); + + // Validate overflow + if total > self.max_size { + return Err(Error::Overflow); + } + + // Success + self.buffer.push(bytes); + + Ok(total) + } + + // Setters + + /// Set new `max_size` value, `DEFAULT_MAX_SIZE` if `None` + pub fn set_max_size(&mut self, value: Option) { + self.max_size = match value { + Some(size) => size, + None => DEFAULT_MAX_SIZE, + } + } + + // Getters + + /// Get reference to bytes collected + pub fn buffer(&self) -> &Vec { + &self.buffer + } + + /// Return copy of bytes as UTF-8 vector + pub fn to_utf8(&self) -> Vec { + self.buffer + .iter() + .flat_map(|byte| byte.iter()) + .cloned() + .collect() + } +} diff --git a/src/client/socket/connection/input/buffer/error.rs b/src/client/buffer/error.rs similarity index 61% rename from src/client/socket/connection/input/buffer/error.rs rename to src/client/buffer/error.rs index 68dd893..82c93e6 100644 --- a/src/client/socket/connection/input/buffer/error.rs +++ b/src/client/buffer/error.rs @@ -1,4 +1,4 @@ pub enum Error { + InputStream, Overflow, - StreamChunkRead, } diff --git a/src/client/response.rs b/src/client/response.rs index ce45f1a..866b4e3 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -6,25 +6,31 @@ pub use body::Body; pub use error::Error; pub use header::Header; +use glib::Bytes; + pub struct Response { header: Header, body: Body, } impl Response { - /// Create new `client::Response` + /// Create new `Self` 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) { + /// Construct from [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + /// + /// Useful for [Gio::InputStream](https://docs.gtk.org/gio/class.InputStream.html): + /// * [read_bytes](https://docs.gtk.org/gio/method.InputStream.read_bytes.html) + /// * [read_bytes_async](https://docs.gtk.org/gio/method.InputStream.read_bytes_async.html) + pub fn from(bytes: &Bytes) -> Result { + let header = match Header::from_response(bytes) { Ok(result) => result, Err(_) => return Err(Error::Header), }; - let body = match Body::from_response(buffer) { + let body = match Body::from_response(bytes) { Ok(result) => result, Err(_) => return Err(Error::Body), }; diff --git a/src/client/response/body.rs b/src/client/response/body.rs index ded304c..80d17b5 100644 --- a/src/client/response/body.rs +++ b/src/client/response/body.rs @@ -1,18 +1,18 @@ pub mod error; pub use error::Error; -use glib::GString; +use glib::{Bytes, 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)?; + // Constructors + pub fn from_response(bytes: &Bytes) -> Result { + let start = Self::start(bytes)?; - let buffer = match response.get(start..) { + let buffer = match bytes.get(start..) { Some(result) => result, None => return Err(Error::Buffer), }; @@ -23,7 +23,7 @@ impl Body { } // Getters - pub fn buffer(&self) -> &Vec { + pub fn buffer(&self) -> &[u8] { &self.buffer } diff --git a/src/client/response/header.rs b/src/client/response/header.rs index ef3da60..5fc953c 100644 --- a/src/client/response/header.rs +++ b/src/client/response/header.rs @@ -8,8 +8,10 @@ pub use meta::Meta; pub use mime::Mime; pub use status::Status; +use glib::Bytes; + pub struct Header { - status: Option, + status: Status, meta: Option, mime: Option, // @TODO @@ -18,35 +20,41 @@ pub struct Header { } 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)?; + // Constructors + pub fn from_response(bytes: &Bytes) -> Result { + // Get header slice of bytes + let end = Self::end(bytes)?; - let buffer = match response.get(..end) { - Some(result) => result, + let bytes = Bytes::from(match bytes.get(..end) { + Some(buffer) => buffer, None => return Err(Error::Buffer), - }; + }); - let meta = match Meta::from_header(buffer) { - Ok(result) => Some(result), - Err(_) => None, - }; + // Status is required, parse to continue + let status = match Status::from_header(&bytes) { + Ok(status) => Ok(status), + Err(reason) => Err(match reason { + status::Error::Decode => Error::StatusDecode, + status::Error::Undefined => Error::StatusUndefined, + }), + }?; - 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) => Some(result), - Err(_) => None, - }; - - Ok(Self { status, meta, mime }) + // Done + Ok(Self { + status, + meta: match Meta::from_header(&bytes) { + Ok(meta) => Some(meta), + Err(_) => None, + }, + mime: match Mime::from_header(&bytes) { + Ok(mime) => Some(mime), + Err(_) => None, + }, + }) } // Getters - pub fn status(&self) -> &Option { + pub fn status(&self) -> &Status { &self.status } @@ -59,8 +67,10 @@ impl Header { } // Tools - fn end(buffer: &[u8]) -> Result { - for (offset, &byte) in buffer.iter().enumerate() { + + /// Get last header byte (until \r) + fn end(bytes: &Bytes) -> Result { + for (offset, &byte) in bytes.iter().enumerate() { if byte == b'\r' { return Ok(offset); } diff --git a/src/client/response/header/error.rs b/src/client/response/header/error.rs index b81311b..4eeff64 100644 --- a/src/client/response/header/error.rs +++ b/src/client/response/header/error.rs @@ -1,5 +1,6 @@ pub enum Error { Buffer, Format, - Status, + StatusDecode, + StatusUndefined, } diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index dc90ea7..b2e9380 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -1,15 +1,18 @@ pub mod error; pub use error::Error; -use glib::GString; +use glib::{Bytes, GString}; +/// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). +/// +/// Usefult to grab placeholder text on 10, 11, 31 codes processing pub struct Meta { buffer: Vec, } impl Meta { - pub fn from_header(buffer: &[u8] /* @TODO */) -> Result { - let buffer = match buffer.get(2..) { + pub fn from_header(bytes: &Bytes) -> Result { + let buffer = match bytes.get(3..) { Some(bytes) => bytes.to_vec(), None => return Err(Error::Undefined), }; @@ -23,4 +26,8 @@ impl Meta { Err(_) => Err(Error::Undefined), } } + + pub fn buffer(&self) -> &[u8] { + &self.buffer + } } diff --git a/src/client/response/header/mime.rs b/src/client/response/header/mime.rs index 1f6ea44..8f9e2d9 100644 --- a/src/client/response/header/mime.rs +++ b/src/client/response/header/mime.rs @@ -1,6 +1,10 @@ -use glib::{GString, Uri}; +pub mod error; +pub use error::Error; + +use glib::{Bytes, GString, Uri}; use std::path::Path; +/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters pub enum Mime { TextGemini, TextPlain, @@ -10,53 +14,58 @@ pub enum Mime { 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? - }) -} +impl Mime { + pub fn from_header(bytes: &Bytes) -> Result { + match bytes.get(..) { + Some(bytes) => match GString::from_utf8(bytes.to_vec()) { + Ok(string) => Self::from_string(string.as_str()), + Err(_) => Err(Error::Decode), + }, + None => Err(Error::Undefined), + } + } -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_path(path: &Path) -> Result { + match path.extension().and_then(|extension| extension.to_str()) { + Some("gmi" | "gemini") => Ok(Self::TextGemini), + Some("txt") => Ok(Self::TextPlain), + Some("png") => Ok(Self::ImagePng), + Some("gif") => Ok(Self::ImageGif), + Some("jpeg" | "jpg") => Ok(Self::ImageJpeg), + Some("webp") => Ok(Self::ImageWebp), + _ => Err(Error::Undefined), + } + } + + pub fn from_string(value: &str) -> Result { + if value.contains("text/gemini") { + return Ok(Self::TextGemini); + } + + if value.contains("text/plain") { + return Ok(Self::TextPlain); + } + + if value.contains("image/gif") { + return Ok(Self::ImageGif); + } + + if value.contains("image/jpeg") { + return Ok(Self::ImageJpeg); + } + + if value.contains("image/webp") { + return Ok(Self::ImageWebp); + } + + if value.contains("image/png") { + return Ok(Self::ImagePng); + } + + Err(Error::Undefined) + } + + pub fn from_uri(uri: &Uri) -> Result { + Self::from_path(Path::new(&uri.to_string())) } } - -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/mime/error.rs b/src/client/response/header/mime/error.rs new file mode 100644 index 0000000..e0c05eb --- /dev/null +++ b/src/client/response/header/mime/error.rs @@ -0,0 +1,4 @@ +pub enum Error { + Decode, + Undefined, +} diff --git a/src/client/response/header/status.rs b/src/client/response/header/status.rs index 841460f..908ddf3 100644 --- a/src/client/response/header/status.rs +++ b/src/client/response/header/status.rs @@ -1,7 +1,7 @@ pub mod error; pub use error::Error; -use glib::GString; +use glib::{Bytes, GString}; /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes pub enum Status { @@ -11,21 +11,23 @@ pub enum Status { 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), +impl Status { + pub fn from_header(bytes: &Bytes) -> Result { + match bytes.get(0..2) { + Some(bytes) => match GString::from_utf8(bytes.to_vec()) { + Ok(string) => Self::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), + pub fn from_string(code: &str) -> Result { + match code { + "10" => Ok(Self::Input), + "11" => Ok(Self::SensitiveInput), + "20" => Ok(Self::Success), + _ => Err(Error::Undefined), + } } } diff --git a/src/client/socket.rs b/src/client/socket.rs deleted file mode 100644 index 4e541f7..0000000 --- a/src/client/socket.rs +++ /dev/null @@ -1,75 +0,0 @@ -pub mod connection; -pub mod error; - -pub use connection::Connection; -pub use error::Error; - -pub const DEFAULT_PORT: u16 = 1965; - -use gio::{ - prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificateFlags, -}; -use glib::Uri; - -pub struct Socket { - client: SocketClient, - default_port: u16, -} - -impl Socket { - // Constructors - - /// Create new `gio::SocketClient` preset for Gemini Protocol - pub fn new() -> Self { - let client = SocketClient::new(); - - client.set_protocol(SocketProtocol::Tcp); - client.set_tls_validation_flags(TlsCertificateFlags::INSECURE); - client.set_tls(true); - - Self { - client, - default_port: DEFAULT_PORT, - } - } - - // Actions - pub fn connect_async( - &self, - uri: Uri, - cancelable: Option, - callback: impl FnOnce(Result) + 'static, - ) { - self.client.connect_to_uri_async( - uri.to_str().as_str(), - self.default_port, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - |result| { - callback(match result { - Ok(connection) => Ok(Connection::new_from(connection)), - Err(_) => Err(Error::Connection), - }) - }, - ); - } - - // Setters - - /// Change default port for this socket connections (`1965` by default) - pub fn set_default_port(&mut self, default_port: u16) { - self.default_port = default_port; - } - - // Getters - - /// Get reference to `gio::SocketClient` - /// - /// https://docs.gtk.org/gio/class.SocketClient.html - pub fn client(&self) -> &SocketClient { - &self.client - } -} diff --git a/src/client/socket/connection.rs b/src/client/socket/connection.rs deleted file mode 100644 index 255a803..0000000 --- a/src/client/socket/connection.rs +++ /dev/null @@ -1,121 +0,0 @@ -pub mod error; -pub mod input; -pub mod output; - -pub use error::Error; -pub use input::Input; -pub use output::Output; - -use gio::{prelude::IOStreamExt, Cancellable, SocketConnection}; -use glib::{gformat, Bytes, Priority, Uri}; - -pub struct Connection { - connection: SocketConnection, -} - -impl Connection { - // Constructors - - /// Create new `Self` from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - pub fn new_from(connection: SocketConnection) -> Self { - Self { connection } - } - - // Actions - - /// Middle-level API to make async socket request for current connection: - /// - /// 1. send request for [Uri](https://docs.gtk.org/glib/struct.Uri.html) - /// to the ouput [OutputStream](https://docs.gtk.org/gio/class.OutputStream.html); - /// 2. write entire [InputStream](https://docs.gtk.org/gio/class.InputStream.html) - /// into `Vec` buffer on success; - /// 3. return taken `Self` with `Result(Vec, Error)` on complete. - pub fn request_async( - self, - uri: Uri, - cancelable: Option, - priority: Option, - chunk: Option, - callback: impl FnOnce(Self, Result, Error>) + 'static, - ) { - Output::new_from_stream(self.connection.output_stream()).write_async( - &Bytes::from(gformat!("{}\r\n", uri.to_str()).as_bytes()), - cancelable.clone(), - priority, - move |output| match output { - Ok(_) => { - Input::new_from_stream(self.connection.input_stream()).read_all_async( - cancelable.clone(), - priority, - chunk, - move |this, input| { - callback( - self, - match input { - Ok(()) => Ok(this.buffer().to_utf8()), - Err(error) => Err(match error { - input::Error::BufferOverflow => Error::InputBufferOverflow, - input::Error::BufferWrite => Error::InputBufferWrite, - input::Error::StreamChunkRead => { - Error::InputStreamChunkRead - } - }), - }, - ); - }, - ); - } - Err(error) => { - callback( - self, - Err(match error { - output::Error::StreamWrite => Error::OutputStreamWrite, - }), - ); - } - }, - ); - } - - /// Asynchronously close current connection - /// - /// Options: - /// * `cancellable` see [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) (`None::<&Cancellable>` by default) - /// * `priority` [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default - /// * `callback` optional function to apply on complete or `None` to skip - pub fn close_async( - &self, - cancelable: Option, - priority: Option, - callback: Option) + 'static>, - ) { - self.connection.close_async( - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - |result| { - if let Some(call) = callback { - call(match result { - Ok(_) => Ok(()), - Err(_) => Err(Error::Close), - }) - } - }, - ); - } - - // Getters - - /// Get reference to `gio::SocketConnection` - /// - /// https://docs.gtk.org/gio/class.SocketConnection.html - pub fn connection(&self) -> &SocketConnection { - &self.connection - } -} diff --git a/src/client/socket/connection/error.rs b/src/client/socket/connection/error.rs deleted file mode 100644 index 7405d6a..0000000 --- a/src/client/socket/connection/error.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub enum Error { - Close, - InputBufferOverflow, - InputBufferWrite, - InputStreamChunkRead, - OutputStreamWrite, -} diff --git a/src/client/socket/connection/input.rs b/src/client/socket/connection/input.rs deleted file mode 100644 index 1c062ee..0000000 --- a/src/client/socket/connection/input.rs +++ /dev/null @@ -1,158 +0,0 @@ -pub mod buffer; -pub mod error; - -pub use buffer::Buffer; -pub use error::Error; - -use gio::{prelude::InputStreamExt, Cancellable, InputStream}; -use glib::Priority; - -pub const DEFAULT_READ_CHUNK: usize = 0x100; - -pub struct Input { - buffer: Buffer, - stream: InputStream, -} - -impl Input { - // Constructors - - /// Create new `Input` from `gio::InputStream` - /// - /// https://docs.gtk.org/gio/class.InputStream.html - pub fn new_from_stream(stream: InputStream) -> Self { - Self { - buffer: Buffer::new(), - stream, - } - } - - // Actions - - /// Synchronously read all bytes from `gio::InputStream` to `input::Buffer` - /// - /// Return `Self` with `buffer` updated on success - /// - /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html - /// * `chunk` max bytes to read per chunk (256 by default) - pub fn read_all( - mut self, - cancelable: Option, - chunk: Option, - ) -> Result { - loop { - // Continue bytes reading - match self.stream.read_bytes( - match chunk { - Some(value) => value, - None => DEFAULT_READ_CHUNK, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - ) { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return Ok(self); - } - - // Save chunk to buffer - match self.buffer.push(bytes) { - Ok(_) => continue, - Err(buffer::Error::Overflow) => return Err(Error::BufferOverflow), - Err(_) => return Err(Error::BufferWrite), - }; - } - Err(_) => return Err(Error::StreamChunkRead), - }; - } - } - - /// Asynchronously read all bytes from `gio::InputStream` to `input::Buffer` - /// - /// * applies `callback` function on last byte reading complete; - /// * return `Self` with `buffer` updated on success - /// - /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) - /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) - /// * `chunk` optional max bytes to read per chunk (`DEFAULT_READ_CHUNK` by default) - /// * `callback` user function to apply on async iteration complete or `None` to skip - pub fn read_all_async( - mut self, - cancelable: Option, - priority: Option, - chunk: Option, - callback: impl FnOnce(Self, Result<(), Error>) + 'static, - ) { - // Continue bytes reading - self.stream.clone().read_bytes_async( - match chunk { - Some(value) => value, - None => DEFAULT_READ_CHUNK, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - move |result| { - match result { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return callback(self, Ok(())); - } - - // Save chunk to buffer - match self.buffer.push(bytes) { - Err(buffer::Error::Overflow) => { - return callback(self, Err(Error::BufferOverflow)) - } - - // Other errors related to write issues @TODO test - Err(_) => return callback(self, Err(Error::BufferWrite)), - - // Async function, nothing to return yet - _ => (), - }; - - // Continue bytes reading... - self.read_all_async(cancelable, priority, chunk, callback); - } - Err(_) => callback(self, Err(Error::StreamChunkRead)), - } - }, - ); - } - - // Setters - - pub fn set_buffer(&mut self, buffer: Buffer) { - self.buffer = buffer; - } - - pub fn set_stream(&mut self, stream: InputStream) { - self.stream = stream; - } - - // Getters - - /// Get reference to `Buffer` - pub fn buffer(&self) -> &Buffer { - &self.buffer - } - - /// Get reference to `gio::InputStream` - pub fn stream(&self) -> &InputStream { - &self.stream - } -} diff --git a/src/client/socket/connection/input/buffer.rs b/src/client/socket/connection/input/buffer.rs deleted file mode 100644 index b9264c7..0000000 --- a/src/client/socket/connection/input/buffer.rs +++ /dev/null @@ -1,87 +0,0 @@ -pub mod error; -pub use error::Error; - -use glib::Bytes; - -pub const DEFAULT_CAPACITY: usize = 0x400; -pub const DEFAULT_MAX_SIZE: usize = 0xfffff; - -pub struct Buffer { - bytes: Vec, - max_size: usize, -} - -impl Buffer { - // Constructors - - /// Create new dynamically allocated `Buffer` with default `capacity` and `max_size` limit - pub fn new() -> Self { - Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) - } - - /// Create new dynamically allocated `Buffer` with options - /// - /// Options: - /// * `capacity` initial bytes request to reduce extra memory overwrites (1024 by default) - /// * `max_size` max bytes to prevent memory overflow (1M by default) - pub fn new_with_options(capacity: Option, max_size: Option) -> Self { - Self { - bytes: Vec::with_capacity(match capacity { - Some(value) => value, - None => DEFAULT_CAPACITY, - }), - max_size: match max_size { - Some(value) => value, - None => DEFAULT_MAX_SIZE, - }, - } - } - - // Setters - - /// Set new `Buffer.max_size` value to prevent memory overflow - /// - /// Use `DEFAULT_MAX_SIZE` if `None` given. - pub fn set_max_size(&mut self, value: Option) { - self.max_size = match value { - Some(size) => size, - None => DEFAULT_MAX_SIZE, - } - } - - // Actions - - /// Push `glib::Bytes` to `Buffer.bytes` - /// - /// Return `Error::Overflow` on `Buffer.max_size` reached. - pub fn push(&mut self, bytes: Bytes) -> Result { - // Calculate new size value - let total = self.bytes.len() + bytes.len(); - - // Validate overflow - if total > self.max_size { - return Err(Error::Overflow); - } - - // Success - self.bytes.push(bytes); - - Ok(total) - } - - // Getters - - /// Get reference to bytes collected - pub fn bytes(&self) -> &Vec { - &self.bytes - } - - /// Return copy of bytes as UTF-8 vector - pub fn to_utf8(&self) -> Vec { - self.bytes - .iter() - .flat_map(|byte| byte.iter()) - .cloned() - .collect() - } -} diff --git a/src/client/socket/connection/input/error.rs b/src/client/socket/connection/input/error.rs deleted file mode 100644 index 1c383fb..0000000 --- a/src/client/socket/connection/input/error.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub enum Error { - BufferOverflow, - BufferWrite, - StreamChunkRead, -} diff --git a/src/client/socket/connection/output.rs b/src/client/socket/connection/output.rs deleted file mode 100644 index d6f1821..0000000 --- a/src/client/socket/connection/output.rs +++ /dev/null @@ -1,71 +0,0 @@ -pub mod error; - -pub use error::Error; - -use gio::{prelude::OutputStreamExt, Cancellable, OutputStream}; -use glib::{Bytes, Priority}; - -pub struct Output { - stream: OutputStream, -} - -impl Output { - // Constructors - - /// Create new `Output` from `gio::OutputStream` - /// - /// https://docs.gtk.org/gio/class.OutputStream.html - pub fn new_from_stream(stream: OutputStream) -> Self { - Self { stream } - } - - // Actions - - /// Asynchronously write all bytes to `gio::OutputStream`, - /// - /// applies `callback` function on last byte sent. - /// - /// Options: - /// * `cancellable` https://docs.gtk.org/gio/class.Cancellable.html (`None::<&Cancellable>` by default) - /// * `priority` e.g. https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html (`Priority::DEFAULT` by default) - /// * `callback` user function to apply on complete - pub fn write_async( - &self, - bytes: &Bytes, - cancelable: Option, - priority: Option, - callback: impl FnOnce(Result) + 'static, - ) { - self.stream.write_bytes_async( - bytes, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - move |result| { - callback(match result { - Ok(size) => Ok(size), - Err(_) => Err(Error::StreamWrite), - }) - }, - ); - } - - // Setters - - pub fn set_stream(&mut self, stream: OutputStream) { - self.stream = stream; - } - - // Getters - - /// Get reference to `gio::OutputStream` - pub fn stream(&self) -> &OutputStream { - &self.stream - } -} diff --git a/src/client/socket/connection/output/error.rs b/src/client/socket/connection/output/error.rs deleted file mode 100644 index 27547a1..0000000 --- a/src/client/socket/connection/output/error.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub enum Error { - StreamWrite, -} diff --git a/src/client/socket/error.rs b/src/client/socket/error.rs deleted file mode 100644 index 1863964..0000000 --- a/src/client/socket/error.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub enum Error { - Connection, -} diff --git a/tests/client.rs b/tests/client.rs index e98122c..1673a59 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -1,21 +1 @@ -use glib::{Uri, UriFlags}; - -#[test] -fn single_socket_request_async() { - // Parse URI - match Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE) { - // Begin async request - Ok(uri) => ggemini::client::single_socket_request_async(uri, |result| match result { - // Process response - Ok(response) => { - // Expect success status - assert!(match response.header().status() { - Some(ggemini::client::response::header::Status::Success) => true, - _ => false, - }) - } - Err(_) => assert!(false), - }), - Err(_) => assert!(false), - } -} // @TODO async +// @TODO From ffb068e398d3e47fcf396706e86d800bc779a2d3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:25:30 +0300 Subject: [PATCH 030/392] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ebc4727..0a59f82 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Glib-oriented network library for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -This library initially created as extension for [Yoda Browser](https://github.com/YGGverse/Yoda), +GGemini (or G-Gemini) initially created as client extension for [Yoda Browser](https://github.com/YGGverse/Yoda), but also could be useful for any other integration as depends of -[glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only. +[glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`v2_66`) crates only. ## Install From 81c1e011e01f205464d19e2613011180fa3bfaea Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:25:36 +0300 Subject: [PATCH 031/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f366386..0cd53b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.4.0" +version = "0.5.0" edition = "2021" license = "MIT" readme = "README.md" From ed038fd0afceac8d12323d5d17aa453ea6576905 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:27:24 +0300 Subject: [PATCH 032/392] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a59f82..65a6b42 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ cargo add ggemini ### `client` -[Gio](https://docs.gtk.org/gio/) API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), -so this Client just bit extends some features for Gemini Protocol. +Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), +this Client just extends a bit some features for Gemini Protocol. #### `client::buffer` From ff5b1619c339193759ee5a6df9d84fe08f818693 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:28:30 +0300 Subject: [PATCH 033/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65a6b42..748f736 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ cargo add ggemini ### `client` Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), -this Client just extends a bit some features for Gemini Protocol. +Client implementation is minimal and just extends a bit some features for Gemini Protocol. #### `client::buffer` From 751d985f9bc2c923a98627f30fe9126f9b8eedda Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:30:11 +0300 Subject: [PATCH 034/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 748f736..2cce847 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ggemini -Glib-oriented network library for [Gemini protocol](https://geminiprotocol.net/) +Glib/Gio-oriented network library for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! From 3459af295cab614058e0e3b2fa8036e9c0dd9c6d Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:32:19 +0300 Subject: [PATCH 035/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2cce847..e37ac03 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ cargo add ggemini ### `client` Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), -Client implementation is minimal and just extends a bit some features for Gemini Protocol. +current `client` implementation is minimal and just extends a bit some features for Gemini Protocol. #### `client::buffer` From 99fd5f42c05236b8bad15648cc4ec60576af7df2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:33:21 +0300 Subject: [PATCH 036/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e37ac03..4be20d2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ cargo add ggemini ### `client` Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), -current `client` implementation is minimal and just extends a bit some features for Gemini Protocol. +`ggemini::client` implementation is minimal and just extends a bit some features for Gemini Protocol. #### `client::buffer` From 2c175cf68482326f3a39d4a3fad8bc4fbf703fed Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:34:03 +0300 Subject: [PATCH 037/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4be20d2..7d0f528 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ cargo add ggemini ### `client` Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), -`ggemini::client` implementation is minimal and just extends a bit some features for Gemini Protocol. +`ggemini::client` implementation just extends a bit some features for Gemini Protocol. #### `client::buffer` From 2585b6ad4ae49f7cf3c6fb4ca0547baefff2c8aa Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:41:32 +0300 Subject: [PATCH 038/392] update readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d0f528..e3c349e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ cargo add ggemini ### `client` Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), -`ggemini::client` implementation just extends a bit some features for Gemini Protocol. +`ggemini::client` just extends some features a bit, to simplify interaction with socket over Gemini Protocol. + +It also contain some children components/mods bellow for low-level access any feature directly. #### `client::buffer` From f83c5babb677b9ad277b90e750733b0e8cf200c7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:42:21 +0300 Subject: [PATCH 039/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3c349e..a1ecef3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network library for [Gemini protocol](https://geminiprotocol.n > Project in development! > -GGemini (or G-Gemini) initially created as client extension for [Yoda Browser](https://github.com/YGGverse/Yoda), +GGemini (or G-Gemini) initially created as the client extension for [Yoda Browser](https://github.com/YGGverse/Yoda), but also could be useful for any other integration as depends of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`v2_66`) crates only. From dccff1e1110e10efd2eda08062e41f015e04d359 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 26 Oct 2024 23:43:14 +0300 Subject: [PATCH 040/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1ecef3..026efb5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Glib/Gio-oriented network library for [Gemini protocol](https://geminiprotocol.n > GGemini (or G-Gemini) initially created as the client extension for [Yoda Browser](https://github.com/YGGverse/Yoda), -but also could be useful for any other integration as depends of +also could be useful for any other integration as depends of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`v2_66`) crates only. ## Install From 9152528790910f975f026b43663a288abe1e55fa Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:27:30 +0200 Subject: [PATCH 041/392] draft new api version --- README.md | 20 +-- src/client.rs | 6 - src/client/buffer.rs | 164 ------------------ src/client/buffer/error.rs | 4 - src/client/error.rs | 6 - src/client/response.rs | 43 ----- src/client/response/body.rs | 187 ++++++++++++++++++--- src/client/response/body/error.rs | 3 +- src/client/response/error.rs | 4 - src/client/response/header.rs | 140 +++++++++++---- src/client/response/header/error.rs | 4 +- src/client/response/header/meta.rs | 14 +- src/client/response/header/meta/error.rs | 1 + src/client/response/header/mime.rs | 9 +- src/client/response/header/mime/error.rs | 1 + src/client/response/header/status.rs | 9 +- src/client/response/header/status/error.rs | 1 + 17 files changed, 299 insertions(+), 317 deletions(-) delete mode 100644 src/client/buffer.rs delete mode 100644 src/client/buffer/error.rs delete mode 100644 src/client/error.rs delete mode 100644 src/client/response/error.rs diff --git a/README.md b/README.md index 026efb5..0443f1e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ # ggemini -Glib/Gio-oriented network library for [Gemini protocol](https://geminiprotocol.net/) +Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! > -GGemini (or G-Gemini) initially created as the client extension for [Yoda Browser](https://github.com/YGGverse/Yoda), -also could be useful for any other integration as depends of -[glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`v2_66`) crates only. +This library mostly written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, +it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only. ## Install @@ -20,23 +19,16 @@ cargo add ggemini ### `client` -Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), -`ggemini::client` just extends some features a bit, to simplify interaction with socket over Gemini Protocol. - -It also contain some children components/mods bellow for low-level access any feature directly. - -#### `client::buffer` +[Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html). +This library just extend some minimal features wanted for Gemini Protocol #### `client::response` -Response parser for [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +Response parser, currently includes low-level interaction API for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) -#### `client::response::Response` #### `client::response::header` #### `client::response::body` -https://docs.gtk.org/glib/struct.Bytes.html - ## See also * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index 2be3ea9..4c6f2cd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,7 +1 @@ -pub mod buffer; -pub mod error; pub mod response; - -pub use buffer::Buffer; -pub use error::Error; -pub use response::Response; diff --git a/src/client/buffer.rs b/src/client/buffer.rs deleted file mode 100644 index f3e8d50..0000000 --- a/src/client/buffer.rs +++ /dev/null @@ -1,164 +0,0 @@ -pub mod error; -pub use error::Error; - -use gio::{ - prelude::{IOStreamExt, InputStreamExt}, - Cancellable, SocketConnection, -}; -use glib::{Bytes, Priority}; - -pub const DEFAULT_CAPACITY: usize = 0x400; -pub const DEFAULT_MAX_SIZE: usize = 0xfffff; - -/// Dynamically allocated [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) buffer -/// with configurable `capacity` and `max_size` limits -pub struct Buffer { - buffer: Vec, - max_size: usize, -} - -impl Buffer { - // Constructors - - /// Create new `Self` with default `capacity` and `max_size` preset - pub fn new() -> Self { - Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) - } - - /// Create new `Self` with options - /// - /// Options: - /// * `capacity` initial bytes request to reduce extra memory reallocation (`DEFAULT_CAPACITY` if `None`) - /// * `max_size` max bytes to prevent memory overflow by unknown stream source (`DEFAULT_MAX_SIZE` if `None`) - pub fn new_with_options(capacity: Option, max_size: Option) -> Self { - Self { - buffer: Vec::with_capacity(match capacity { - Some(value) => value, - None => DEFAULT_CAPACITY, - }), - max_size: match max_size { - Some(value) => value, - None => DEFAULT_MAX_SIZE, - }, - } - } - - // Intentable constructors - - /// Simplest way to create `Self` buffer from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - /// - /// Options: - /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from - /// * `callback` function to apply on all async operations complete, return `Result)>` - pub fn from_connection_async( - connection: SocketConnection, - callback: impl FnOnce(Result)>) + 'static, - ) { - Self::read_all_async(Self::new(), connection, None, None, None, callback); - } - - // Actions - - /// Asynchronously read all [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) - /// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to `Self.buffer` - /// - /// Useful to grab entire stream without risk of memory overflow (according to `Self.max_size`), - /// reduce extra memory reallocations by `capacity` option. - /// - /// **Notes** - /// - /// We are using entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) reference - /// instead of [InputStream](https://docs.gtk.org/gio/class.InputStream.html) directly just to keep main connection alive in the async context - /// - /// **Options** - /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from - /// * `cancellable` - [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) or `None::<&Cancellable>` by default - /// * `priority` - [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default - /// * `chunk` optional bytes count to read per chunk (`0x100` by default) - /// * `callback` function to apply on all async operations complete, return `Result)>` - pub fn read_all_async( - mut self, - connection: SocketConnection, - cancelable: Option, - priority: Option, - chunk: Option, - callback: impl FnOnce(Result)>) + 'static, - ) { - connection.input_stream().read_bytes_async( - match chunk { - Some(value) => value, - None => 0x100, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - move |result| match result { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return callback(Ok(self)); - } - - // Save chunk to buffer - if let Err(reason) = self.push(bytes) { - return callback(Err((reason, None))); - }; - - // Continue bytes read.. - self.read_all_async(connection, cancelable, priority, chunk, callback); - } - Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), - }, - ); - } - - /// Push [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to `Self.buffer` - /// - /// Return `Error::Overflow` on `max_size` reached - pub fn push(&mut self, bytes: Bytes) -> Result { - // Calculate new size value - let total = self.buffer.len() + bytes.len(); - - // Validate overflow - if total > self.max_size { - return Err(Error::Overflow); - } - - // Success - self.buffer.push(bytes); - - Ok(total) - } - - // Setters - - /// Set new `max_size` value, `DEFAULT_MAX_SIZE` if `None` - pub fn set_max_size(&mut self, value: Option) { - self.max_size = match value { - Some(size) => size, - None => DEFAULT_MAX_SIZE, - } - } - - // Getters - - /// Get reference to bytes collected - pub fn buffer(&self) -> &Vec { - &self.buffer - } - - /// Return copy of bytes as UTF-8 vector - pub fn to_utf8(&self) -> Vec { - self.buffer - .iter() - .flat_map(|byte| byte.iter()) - .cloned() - .collect() - } -} diff --git a/src/client/buffer/error.rs b/src/client/buffer/error.rs deleted file mode 100644 index 82c93e6..0000000 --- a/src/client/buffer/error.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub enum Error { - InputStream, - Overflow, -} diff --git a/src/client/error.rs b/src/client/error.rs deleted file mode 100644 index 10cda29..0000000 --- a/src/client/error.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub enum Error { - Close, - Connection, - Request, - Response, -} diff --git a/src/client/response.rs b/src/client/response.rs index 866b4e3..a6016ba 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,48 +1,5 @@ pub mod body; -pub mod error; pub mod header; pub use body::Body; -pub use error::Error; pub use header::Header; - -use glib::Bytes; - -pub struct Response { - header: Header, - body: Body, -} - -impl Response { - /// Create new `Self` - pub fn new(header: Header, body: Body) -> Self { - Self { header, body } - } - - /// Construct from [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) - /// - /// Useful for [Gio::InputStream](https://docs.gtk.org/gio/class.InputStream.html): - /// * [read_bytes](https://docs.gtk.org/gio/method.InputStream.read_bytes.html) - /// * [read_bytes_async](https://docs.gtk.org/gio/method.InputStream.read_bytes_async.html) - pub fn from(bytes: &Bytes) -> Result { - let header = match Header::from_response(bytes) { - Ok(result) => result, - Err(_) => return Err(Error::Header), - }; - - let body = match Body::from_response(bytes) { - 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 index 80d17b5..5d59124 100644 --- a/src/client/response/body.rs +++ b/src/client/response/body.rs @@ -1,46 +1,187 @@ pub mod error; pub use error::Error; -use glib::{Bytes, GString}; +use gio::{ + prelude::{IOStreamExt, InputStreamExt}, + Cancellable, SocketConnection, +}; +use glib::{Bytes, GString, Priority}; +pub const DEFAULT_CAPACITY: usize = 0x400; +pub const DEFAULT_MAX_SIZE: usize = 0xfffff; + +/// Body container with memory-overflow-safe, dynamically allocated [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) buffer +/// +/// **Features** +/// +/// * configurable `capacity` and `max_size` options +/// * build-in [InputStream](https://docs.gtk.org/gio/class.InputStream.html) parser +/// +/// **Notice** +/// +/// * Recommended for gemtext documents +/// * For media types, use native stream processors (e.g. [Pixbuf](https://docs.gtk.org/gdk-pixbuf/ctor.Pixbuf.new_from_stream.html) for images) pub struct Body { - buffer: Vec, + buffer: Vec, + max_size: usize, } impl Body { // Constructors - pub fn from_response(bytes: &Bytes) -> Result { - let start = Self::start(bytes)?; - let buffer = match bytes.get(start..) { - Some(result) => result, - None => return Err(Error::Buffer), - }; + /// Create new empty `Self` with default `capacity` and `max_size` preset + pub fn new() -> Self { + Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) + } - Ok(Self { - buffer: Vec::from(buffer), - }) + /// Create new new `Self` with options + /// + /// Options: + /// * `capacity` initial bytes request to reduce extra memory reallocation (`DEFAULT_CAPACITY` if `None`) + /// * `max_size` max bytes to prevent memory overflow by unknown stream source (`DEFAULT_MAX_SIZE` if `None`) + pub fn new_with_options(capacity: Option, max_size: Option) -> Self { + Self { + buffer: Vec::with_capacity(match capacity { + Some(value) => value, + None => DEFAULT_CAPACITY, + }), + max_size: match max_size { + Some(value) => value, + None => DEFAULT_MAX_SIZE, + }, + } + } + + /// Simple way to create `Self` buffer from active [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + /// + /// **Options** + /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from + /// * `callback` function to apply on async operations complete, return `Result)>` + /// + /// **Notes** + /// + /// * method requires entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html), + /// not just [InputStream](https://docs.gtk.org/gio/class.InputStream.html) because of async features; + /// * use this method after `Header` bytes taken from input stream connected (otherwise, take a look on high-level `Response` parser) + pub fn from_socket_connection_async( + connection: SocketConnection, + callback: impl FnOnce(Result)>) + 'static, + ) { + Self::read_all_async(Self::new(), connection, None, None, None, callback); + } + + // Actions + + /// Asynchronously read all [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + /// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to `Self.buffer` by `chunk` + /// + /// Useful to grab entire stream without risk of memory overflow (according to `Self.max_size`), + /// reduce extra memory reallocations by `capacity` option. + /// + /// **Notes** + /// + /// We are using entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) reference + /// instead of [InputStream](https://docs.gtk.org/gio/class.InputStream.html) just to keep main connection alive in the async chunks context + /// + /// **Options** + /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from + /// * `cancellable` - [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) or `None::<&Cancellable>` by default + /// * `priority` - [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default + /// * `chunk` optional bytes count to read per chunk (`0x100` by default) + /// * `callback` function to apply on all async operations complete, return `Result)>` + pub fn read_all_async( + mut self, + connection: SocketConnection, + cancelable: Option, + priority: Option, + chunk: Option, + callback: impl FnOnce(Result)>) + 'static, + ) { + connection.input_stream().read_bytes_async( + match chunk { + Some(value) => value, + None => 0x100, + }, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + match cancelable.clone() { + Some(value) => Some(value), + None => None::, + } + .as_ref(), + move |result| match result { + Ok(bytes) => { + // No bytes were read, end of stream + if bytes.len() == 0 { + return callback(Ok(self)); + } + + // Save chunk to buffer + if let Err(reason) = self.push(bytes) { + return callback(Err((reason, None))); + }; + + // Continue bytes read.. + self.read_all_async(connection, cancelable, priority, chunk, callback); + } + Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), + }, + ); + } + + /// Push [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to `Self.buffer` + /// + /// Return `Error::Overflow` on `max_size` reached + pub fn push(&mut self, bytes: Bytes) -> Result { + // Calculate new size value + let total = self.buffer.len() + bytes.len(); + + // Validate overflow + if total > self.max_size { + return Err(Error::Overflow); + } + + // Success + self.buffer.push(bytes); + + Ok(total) + } + + // Setters + + /// Set new `max_size` value, `DEFAULT_MAX_SIZE` if `None` + pub fn set_max_size(&mut self, value: Option) { + self.max_size = match value { + Some(size) => size, + None => DEFAULT_MAX_SIZE, + } } // Getters - pub fn buffer(&self) -> &[u8] { + + /// Get reference to `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) collected + pub fn buffer(&self) -> &Vec { &self.buffer } + /// Return copy of `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) as UTF-8 vector + pub fn to_utf8(&self) -> Vec { + self.buffer + .iter() + .flat_map(|byte| byte.iter()) + .cloned() + .collect() + } + + // Intentable getters + + /// Try convert `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to GString pub fn to_gstring(&self) -> Result { - match GString::from_utf8(self.buffer.to_vec()) { + match GString::from_utf8(self.to_utf8()) { 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 index 3284dea..69a71fc 100644 --- a/src/client/response/body/error.rs +++ b/src/client/response/body/error.rs @@ -2,5 +2,6 @@ pub enum Error { Buffer, Decode, Format, - Status, + InputStream, + Overflow, } diff --git a/src/client/response/error.rs b/src/client/response/error.rs deleted file mode 100644 index 089f4ae..0000000 --- a/src/client/response/error.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub enum Error { - Header, - Body, -} diff --git a/src/client/response/header.rs b/src/client/response/header.rs index 5fc953c..c673975 100644 --- a/src/client/response/header.rs +++ b/src/client/response/header.rs @@ -8,7 +8,11 @@ pub use meta::Meta; pub use mime::Mime; pub use status::Status; -use glib::Bytes; +use gio::{ + prelude::{IOStreamExt, InputStreamExt}, + Cancellable, SocketConnection, +}; +use glib::{Bytes, Priority}; pub struct Header { status: Status, @@ -21,39 +25,58 @@ pub struct Header { impl Header { // Constructors - pub fn from_response(bytes: &Bytes) -> Result { - // Get header slice of bytes - let end = Self::end(bytes)?; - let bytes = Bytes::from(match bytes.get(..end) { - Some(buffer) => buffer, - None => return Err(Error::Buffer), - }); - - // Status is required, parse to continue - let status = match Status::from_header(&bytes) { - Ok(status) => Ok(status), - Err(reason) => Err(match reason { - status::Error::Decode => Error::StatusDecode, - status::Error::Undefined => Error::StatusUndefined, - }), - }?; - - // Done - Ok(Self { - status, - meta: match Meta::from_header(&bytes) { - Ok(meta) => Some(meta), - Err(_) => None, + pub fn from_socket_connection_async( + socket_connection: SocketConnection, + priority: Option, + cancellable: Option, + callback: impl FnOnce(Result)>) + 'static, + ) { + // Take header buffer from input stream + Self::read_from_socket_connection_async( + Vec::with_capacity(1024), + socket_connection, + match cancellable { + Some(value) => Some(value), + None => None::, }, - mime: match Mime::from_header(&bytes) { - Ok(mime) => Some(mime), - Err(_) => None, + match priority { + Some(value) => value, + None => Priority::DEFAULT, }, - }) + |result| { + callback(match result { + Ok(buffer) => { + // Status is required, parse to continue + match Status::from_header(&buffer) { + Ok(status) => Ok(Self { + status, + meta: match Meta::from_header(&buffer) { + Ok(meta) => Some(meta), + Err(_) => None, + }, + mime: match Mime::from_header(&buffer) { + Ok(mime) => Some(mime), + Err(_) => None, + }, + }), + Err(reason) => Err(( + match reason { + status::Error::Decode => Error::StatusDecode, + status::Error::Undefined => Error::StatusUndefined, + }, + None, + )), + } + } + Err(error) => Err(error), + }) + }, + ); } // Getters + pub fn status(&self) -> &Status { &self.status } @@ -68,13 +91,58 @@ impl Header { // Tools - /// Get last header byte (until \r) - fn end(bytes: &Bytes) -> Result { - for (offset, &byte) in bytes.iter().enumerate() { - if byte == b'\r' { - return Ok(offset); - } - } - Err(Error::Format) + pub fn read_from_socket_connection_async( + mut buffer: Vec, + connection: SocketConnection, + cancellable: Option, + priority: Priority, + callback: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, + ) { + connection.input_stream().read_bytes_async( + 1, // do not change! + priority, + cancellable.clone().as_ref(), + move |result| match result { + Ok(bytes) => { + // Expect valid header length + if bytes.len() == 0 || buffer.len() + 1 > 1024 { + return callback(Err((Error::Protocol, None))); + } + + // Read next byte without buffer record + if bytes.contains(&b'\r') { + return Self::read_from_socket_connection_async( + buffer, + connection, + cancellable, + priority, + callback, + ); + } + + // Complete without buffer record + if bytes.contains(&b'\n') { + return callback(Ok(buffer + .iter() + .flat_map(|byte| byte.iter()) + .cloned() + .collect())); // convert to UTF-8 + } + + // Record + buffer.push(bytes); + + // Continue + Self::read_from_socket_connection_async( + buffer, + connection, + cancellable, + priority, + callback, + ); + } + Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), + }, + ); } } diff --git a/src/client/response/header/error.rs b/src/client/response/header/error.rs index 4eeff64..e8176bd 100644 --- a/src/client/response/header/error.rs +++ b/src/client/response/header/error.rs @@ -1,6 +1,8 @@ +#[derive(Debug)] pub enum Error { Buffer, - Format, + InputStream, + Protocol, StatusDecode, StatusUndefined, } diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index b2e9380..17bf70e 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -1,7 +1,7 @@ pub mod error; pub use error::Error; -use glib::{Bytes, GString}; +use glib::GString; /// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). /// @@ -11,13 +11,13 @@ pub struct Meta { } impl Meta { - pub fn from_header(bytes: &Bytes) -> Result { - let buffer = match bytes.get(3..) { - Some(bytes) => bytes.to_vec(), + pub fn from_header(buffer: &[u8]) -> Result { + match buffer.get(3..) { + Some(value) => Ok(Self { + buffer: value.to_vec(), + }), None => return Err(Error::Undefined), - }; - - Ok(Self { buffer }) + } } pub fn to_gstring(&self) -> Result { diff --git a/src/client/response/header/meta/error.rs b/src/client/response/header/meta/error.rs index e0c05eb..d9b19fd 100644 --- a/src/client/response/header/meta/error.rs +++ b/src/client/response/header/meta/error.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub enum Error { Decode, Undefined, diff --git a/src/client/response/header/mime.rs b/src/client/response/header/mime.rs index 8f9e2d9..c3cac44 100644 --- a/src/client/response/header/mime.rs +++ b/src/client/response/header/mime.rs @@ -1,10 +1,11 @@ pub mod error; pub use error::Error; -use glib::{Bytes, GString, Uri}; +use glib::{GString, Uri}; use std::path::Path; /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters +#[derive(Debug)] pub enum Mime { TextGemini, TextPlain, @@ -15,9 +16,9 @@ pub enum Mime { } // @TODO impl Mime { - pub fn from_header(bytes: &Bytes) -> Result { - match bytes.get(..) { - Some(bytes) => match GString::from_utf8(bytes.to_vec()) { + pub fn from_header(buffer: &[u8]) -> Result { + match buffer.get(..) { + Some(value) => match GString::from_utf8(value.to_vec()) { Ok(string) => Self::from_string(string.as_str()), Err(_) => Err(Error::Decode), }, diff --git a/src/client/response/header/mime/error.rs b/src/client/response/header/mime/error.rs index e0c05eb..d9b19fd 100644 --- a/src/client/response/header/mime/error.rs +++ b/src/client/response/header/mime/error.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub enum Error { Decode, Undefined, diff --git a/src/client/response/header/status.rs b/src/client/response/header/status.rs index 908ddf3..f251d3b 100644 --- a/src/client/response/header/status.rs +++ b/src/client/response/header/status.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -use glib::{Bytes, GString}; +use glib::GString; /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes +#[derive(Debug)] pub enum Status { Input, SensitiveInput, @@ -12,9 +13,9 @@ pub enum Status { } // @TODO impl Status { - pub fn from_header(bytes: &Bytes) -> Result { - match bytes.get(0..2) { - Some(bytes) => match GString::from_utf8(bytes.to_vec()) { + pub fn from_header(buffer: &[u8]) -> Result { + match buffer.get(0..2) { + Some(value) => match GString::from_utf8(value.to_vec()) { Ok(string) => Self::from_string(string.as_str()), Err(_) => Err(Error::Decode), }, diff --git a/src/client/response/header/status/error.rs b/src/client/response/header/status/error.rs index e0c05eb..d9b19fd 100644 --- a/src/client/response/header/status/error.rs +++ b/src/client/response/header/status/error.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub enum Error { Decode, Undefined, From d707143cabdd95cb4c8f6be137cac47f072e1736 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:29:19 +0200 Subject: [PATCH 042/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0443f1e..fecd812 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ggemini -Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/) +Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] > Project in development! From e71d531e4642d7cc126acc2d3681ca10ea05b80e Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:31:19 +0200 Subject: [PATCH 043/392] update readme --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index fecd812..c585108 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,6 @@ cargo add ggemini This library just extend some minimal features wanted for Gemini Protocol #### `client::response` - -Response parser, currently includes low-level interaction API for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - #### `client::response::header` #### `client::response::body` From f2da8044b18dfb3a6bceb6bc47ed91148ef4d024 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:32:41 +0200 Subject: [PATCH 044/392] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c585108..b29aeb7 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ cargo add ggemini ### `client` -[Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html). -This library just extend some minimal features wanted for Gemini Protocol +[Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ +This library just extends some minimal features wanted for Gemini Protocol. #### `client::response` #### `client::response::header` From 0404ac46429463fe86b01f21bf4ae4681ba5ac95 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:33:56 +0200 Subject: [PATCH 045/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b29aeb7..2bb0a8f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -This library mostly written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, +This library written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only. ## Install From 71d3de47d5988410b1f89817de4ebf217354fa61 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:36:43 +0200 Subject: [PATCH 046/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bb0a8f..ad9f550 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ cargo add ggemini ### `client` [Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ -This library just extends some minimal features wanted for Gemini Protocol. +`client` collection just extends some minimal features wanted for Gemini Protocol. #### `client::response` #### `client::response::header` From 42441ebb242d62c599274180330e9096f44222e7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:37:34 +0200 Subject: [PATCH 047/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad9f550..b1fd96d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > This library written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, -it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only. +it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only ## Install From 6514eb5919f6b5c962b89172e57e9cfc9f4602aa Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:39:06 +0200 Subject: [PATCH 048/392] update readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b1fd96d..5843161 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ cargo add ggemini ### `client` [Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ -`client` collection just extends some minimal features wanted for Gemini Protocol. +`client` collection just extends some minimal features wanted for Gemini Protocol interaction. + +_todo_ #### `client::response` #### `client::response::header` From e7f4d11aaed5647d3829f2d237fb28e8ee068ffd Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:39:45 +0200 Subject: [PATCH 049/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5843161..3680152 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ cargo add ggemini ### `client` [Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ -`client` collection just extends some minimal features wanted for Gemini Protocol interaction. +`client` collection just extends some features wanted for Gemini Protocol interaction. _todo_ From c413b8239e312ca42b4e2e969d95f7d9a173f184 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:40:54 +0200 Subject: [PATCH 050/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3680152..bf40a9f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -This library written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, +GGemini (or G-Gemini) library written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only ## Install From 5a30b63a27c4fc2bf4d9cbb427d00559374adfbd Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:41:42 +0200 Subject: [PATCH 051/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf40a9f..38a9995 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only ## Install From b2bceb775a990364cb574373cc89766d11356db0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 02:55:25 +0200 Subject: [PATCH 052/392] update error enum --- src/client/response/body.rs | 4 ++-- src/client/response/body/error.rs | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/client/response/body.rs b/src/client/response/body.rs index 5d59124..2d061b5 100644 --- a/src/client/response/body.rs +++ b/src/client/response/body.rs @@ -126,7 +126,7 @@ impl Body { // Continue bytes read.. self.read_all_async(connection, cancelable, priority, chunk, callback); } - Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), + Err(reason) => callback(Err((Error::InputStreamRead, Some(reason.message())))), }, ); } @@ -140,7 +140,7 @@ impl Body { // Validate overflow if total > self.max_size { - return Err(Error::Overflow); + return Err(Error::BufferOverflow); } // Success diff --git a/src/client/response/body/error.rs b/src/client/response/body/error.rs index 69a71fc..b90b9c2 100644 --- a/src/client/response/body/error.rs +++ b/src/client/response/body/error.rs @@ -1,7 +1,5 @@ pub enum Error { - Buffer, Decode, - Format, - InputStream, - Overflow, + InputStreamRead, + BufferOverflow, } From 2a01232f6a2117f7ed66b23f94f3797764a79fbe Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 03:07:12 +0200 Subject: [PATCH 053/392] rename entities --- src/client/response/body.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/client/response/body.rs b/src/client/response/body.rs index 2d061b5..a00eda3 100644 --- a/src/client/response/body.rs +++ b/src/client/response/body.rs @@ -55,7 +55,7 @@ impl Body { /// Simple way to create `Self` buffer from active [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// /// **Options** - /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from + /// * `socket_connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from /// * `callback` function to apply on async operations complete, return `Result)>` /// /// **Notes** @@ -64,10 +64,17 @@ impl Body { /// not just [InputStream](https://docs.gtk.org/gio/class.InputStream.html) because of async features; /// * use this method after `Header` bytes taken from input stream connected (otherwise, take a look on high-level `Response` parser) pub fn from_socket_connection_async( - connection: SocketConnection, + socket_connection: SocketConnection, callback: impl FnOnce(Result)>) + 'static, ) { - Self::read_all_async(Self::new(), connection, None, None, None, callback); + Self::read_all_from_socket_connection_async( + Self::new(), + socket_connection, + None, + None, + None, + callback, + ); } // Actions @@ -84,20 +91,20 @@ impl Body { /// instead of [InputStream](https://docs.gtk.org/gio/class.InputStream.html) just to keep main connection alive in the async chunks context /// /// **Options** - /// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from + /// * `socket_connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from /// * `cancellable` - [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) or `None::<&Cancellable>` by default /// * `priority` - [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default /// * `chunk` optional bytes count to read per chunk (`0x100` by default) /// * `callback` function to apply on all async operations complete, return `Result)>` - pub fn read_all_async( + pub fn read_all_from_socket_connection_async( mut self, - connection: SocketConnection, + socket_connection: SocketConnection, cancelable: Option, priority: Option, chunk: Option, callback: impl FnOnce(Result)>) + 'static, ) { - connection.input_stream().read_bytes_async( + socket_connection.input_stream().read_bytes_async( match chunk { Some(value) => value, None => 0x100, @@ -124,7 +131,13 @@ impl Body { }; // Continue bytes read.. - self.read_all_async(connection, cancelable, priority, chunk, callback); + self.read_all_from_socket_connection_async( + socket_connection, + cancelable, + priority, + chunk, + callback, + ); } Err(reason) => callback(Err((Error::InputStreamRead, Some(reason.message())))), }, From 7b0bc23a9f3fe9d37acb6c68f0ede50a0901f1df Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 13:40:16 +0200 Subject: [PATCH 054/392] add HEADER_BYTES_LEN --- src/client/response/header.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/response/header.rs b/src/client/response/header.rs index c673975..1b73346 100644 --- a/src/client/response/header.rs +++ b/src/client/response/header.rs @@ -14,6 +14,8 @@ use gio::{ }; use glib::{Bytes, Priority}; +pub const HEADER_BYTES_LEN: usize = 0x400; // 1024 + pub struct Header { status: Status, meta: Option, @@ -34,7 +36,7 @@ impl Header { ) { // Take header buffer from input stream Self::read_from_socket_connection_async( - Vec::with_capacity(1024), + Vec::with_capacity(HEADER_BYTES_LEN), socket_connection, match cancellable { Some(value) => Some(value), @@ -105,7 +107,7 @@ impl Header { move |result| match result { Ok(bytes) => { // Expect valid header length - if bytes.len() == 0 || buffer.len() + 1 > 1024 { + if bytes.len() == 0 || buffer.len() >= HEADER_BYTES_LEN { return callback(Err((Error::Protocol, None))); } From a26e59d6425c01748a3308f883dc0aee6c9be419 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 13:48:25 +0200 Subject: [PATCH 055/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 38a9995..dfea003 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ cargo add ggemini ### `client` -[Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ +[Gio](https://docs.gtk.org/gio/) API already provides powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ `client` collection just extends some features wanted for Gemini Protocol interaction. _todo_ From 8e516b534def45eb7d14beafbf78717623158a46 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 14:25:22 +0200 Subject: [PATCH 056/392] fix redirect status detection --- Cargo.toml | 2 +- src/client/response/header/status.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0cd53b6..3c35c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.5.0" +version = "0.5.1" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/client/response/header/status.rs b/src/client/response/header/status.rs index f251d3b..6097171 100644 --- a/src/client/response/header/status.rs +++ b/src/client/response/header/status.rs @@ -6,10 +6,14 @@ use glib::GString; /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes #[derive(Debug)] pub enum Status { + // 10 | 11 Input, SensitiveInput, + // 20 Success, + // 30 | 31 Redirect, + PermanentRedirect, } // @TODO impl Status { @@ -28,6 +32,8 @@ impl Status { "10" => Ok(Self::Input), "11" => Ok(Self::SensitiveInput), "20" => Ok(Self::Success), + "30" => Ok(Self::Redirect), + "31" => Ok(Self::PermanentRedirect), _ => Err(Error::Undefined), } } From 338d0b7b6ffb7bf0c537a00abb0358eb74204e82 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 15:22:32 +0200 Subject: [PATCH 057/392] add parsing category --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3c35c4c..d5d0ab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT" readme = "README.md" description = "Glib-oriented client for Gemini protocol" keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"] -categories = ["development-tools", "network-programming"] +categories = ["development-tools", "network-programming", "parsing"] repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] From 64cc68e9bd5e225de3467b3e3f4c260b92413b23 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 20:36:37 +0200 Subject: [PATCH 058/392] add audio formats --- src/client/response/header/mime.rs | 31 ++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/client/response/header/mime.rs b/src/client/response/header/mime.rs index c3cac44..9f81a91 100644 --- a/src/client/response/header/mime.rs +++ b/src/client/response/header/mime.rs @@ -7,12 +7,18 @@ use std::path::Path; /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters #[derive(Debug)] pub enum Mime { + // Text TextGemini, TextPlain, - ImagePng, + // Image ImageGif, ImageJpeg, + ImagePng, ImageWebp, + // Audio + AudioFlac, + AudioMpeg, + AudioOgg, } // @TODO impl Mime { @@ -28,17 +34,24 @@ impl Mime { pub fn from_path(path: &Path) -> Result { match path.extension().and_then(|extension| extension.to_str()) { + // Text Some("gmi" | "gemini") => Ok(Self::TextGemini), + // Image Some("txt") => Ok(Self::TextPlain), Some("png") => Ok(Self::ImagePng), Some("gif") => Ok(Self::ImageGif), Some("jpeg" | "jpg") => Ok(Self::ImageJpeg), Some("webp") => Ok(Self::ImageWebp), + // Audio + Some("flac") => Ok(Self::AudioFlac), + Some("mp3") => Ok(Self::AudioMpeg), + Some("ogg" | "opus" | "oga" | "spx") => Ok(Self::AudioOgg), _ => Err(Error::Undefined), - } + } // @TODO extension to lowercase } pub fn from_string(value: &str) -> Result { + // Text if value.contains("text/gemini") { return Ok(Self::TextGemini); } @@ -47,6 +60,7 @@ impl Mime { return Ok(Self::TextPlain); } + // Image if value.contains("image/gif") { return Ok(Self::ImageGif); } @@ -63,6 +77,19 @@ impl Mime { return Ok(Self::ImagePng); } + // Audio + if value.contains("audio/flac") { + return Ok(Self::AudioFlac); + } + + if value.contains("audio/mpeg") { + return Ok(Self::AudioMpeg); + } + + if value.contains("audio/ogg") { + return Ok(Self::AudioOgg); + } + Err(Error::Undefined) } From 0bbcb6372d3940d096ef5e2d4add5aa1b792fd3b Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 28 Oct 2024 20:38:27 +0200 Subject: [PATCH 059/392] reorder asc --- src/client/response/header/mime.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/response/header/mime.rs b/src/client/response/header/mime.rs index 9f81a91..73044d9 100644 --- a/src/client/response/header/mime.rs +++ b/src/client/response/header/mime.rs @@ -36,16 +36,16 @@ impl Mime { match path.extension().and_then(|extension| extension.to_str()) { // Text Some("gmi" | "gemini") => Ok(Self::TextGemini), - // Image Some("txt") => Ok(Self::TextPlain), - Some("png") => Ok(Self::ImagePng), + // Image Some("gif") => Ok(Self::ImageGif), Some("jpeg" | "jpg") => Ok(Self::ImageJpeg), + Some("png") => Ok(Self::ImagePng), Some("webp") => Ok(Self::ImageWebp), // Audio Some("flac") => Ok(Self::AudioFlac), Some("mp3") => Ok(Self::AudioMpeg), - Some("ogg" | "opus" | "oga" | "spx") => Ok(Self::AudioOgg), + Some("oga" | "ogg" | "opus" | "spx") => Ok(Self::AudioOgg), _ => Err(Error::Undefined), } // @TODO extension to lowercase } From 02ced24cce23ed61d9ec6a2f700315f5d2885ba4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 29 Oct 2024 16:01:05 +0200 Subject: [PATCH 060/392] implement gio::memory_input_stream --- README.md | 8 +- src/gio.rs | 1 + src/gio/memory_input_stream.rs | 141 +++++++++++++++++++++++++++ src/gio/memory_input_stream/error.rs | 4 + src/lib.rs | 1 + 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/gio.rs create mode 100644 src/gio/memory_input_stream.rs create mode 100644 src/gio/memory_input_stream/error.rs diff --git a/README.md b/README.md index dfea003..bcc4a0d 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,21 @@ cargo add ggemini ## Usage +_todo_ + ### `client` [Gio](https://docs.gtk.org/gio/) API already provides powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ `client` collection just extends some features wanted for Gemini Protocol interaction. -_todo_ - #### `client::response` #### `client::response::header` #### `client::response::body` +### `gio` + +#### `gio::memory_input_stream` + ## See also * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file diff --git a/src/gio.rs b/src/gio.rs new file mode 100644 index 0000000..c20d929 --- /dev/null +++ b/src/gio.rs @@ -0,0 +1 @@ +pub mod memory_input_stream; diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs new file mode 100644 index 0000000..202c3a2 --- /dev/null +++ b/src/gio/memory_input_stream.rs @@ -0,0 +1,141 @@ +pub mod error; +pub use error::Error; + +use gio::{ + prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, + Cancellable, MemoryInputStream, SocketConnection, +}; +use glib::{Bytes, Priority}; + +/// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) +/// from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// +/// Useful to create dynamically allocated, memory-safe buffer +/// from remote connections, where final size of target data could not be known by Gemini protocol restrictions. +/// Also, could be useful for [Pixbuf](https://docs.gtk.org/gdk-pixbuf/class.Pixbuf.html) or +/// loading widgets like [Spinner](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Spinner.html) +/// to display bytes on async data loading. +/// +/// * this function takes entire `SocketConnection` reference (not `MemoryInputStream`) just to keep connection alive in the async context +/// +/// **Implementation** +/// +/// Implements low-level `read_all_from_socket_connection_async` function: +/// * recursively read all bytes from `InputStream` for `SocketConnection` according to `bytes_in_chunk` argument +/// * calculates total bytes length on every chunk iteration, validate sum with `bytes_total_limit` argument +/// * stop reading `InputStream` with `Result` on zero bytes in chunk received +/// * applies optional callback functions: +/// * `on_chunk` - return reference to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) and `bytes_total` collected for every chunk in reading loop +/// * `on_complete` - return `MemoryInputStream` on success or `Error` on failure as `Result` +pub fn from_socket_connection_async( + socket_connection: SocketConnection, + cancelable: Option, + priority: Priority, + bytes_in_chunk: usize, + bytes_total_limit: usize, + on_chunk: Option, + on_complete: Option)>) + 'static>, +) { + read_all_from_socket_connection_async( + MemoryInputStream::new(), + socket_connection, + cancelable, + priority, + bytes_in_chunk, + bytes_total_limit, + 0, // initial `bytes_total` value + on_chunk, + on_complete, + ); +} + +/// Low-level helper for `from_socket_connection_async` function, +/// also provides public API for external usage. +/// +/// Asynchronously read [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +/// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// to given [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html). +/// Applies optional `on_chunk` and `on_complete` callback functions. +/// +/// Useful to create dynamically allocated, memory-safe buffer +/// from remote connections, where final size of target data could not be known by Gemini protocol restrictions. +/// Also, could be useful for [Pixbuf](https://docs.gtk.org/gdk-pixbuf/class.Pixbuf.html) or +/// loading widgets like [Spinner](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Spinner.html) +/// to display bytes on async data loading. +/// +/// * this function takes entire `SocketConnection` reference (not `MemoryInputStream`) just to keep connection alive in the async context +/// +/// **Implementation** +/// +/// * recursively read all bytes from `InputStream` for `SocketConnection` according to `bytes_in_chunk` argument +/// * calculates total bytes length on every chunk iteration, validate sum with `bytes_total_limit` argument +/// * stop reading `InputStream` with `Result` on zero bytes in chunk received, otherwise continue next chunk request in loop +/// * applies optional callback functions: +/// * `on_chunk` - return reference to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) and `bytes_total` collected for every chunk in reading loop +/// * `on_complete` - return `MemoryInputStream` on success or `Error` on failure as `Result` +pub fn read_all_from_socket_connection_async( + memory_input_stream: MemoryInputStream, + socket_connection: SocketConnection, + cancelable: Option, + priority: Priority, + bytes_in_chunk: usize, + bytes_total_limit: usize, + bytes_total: usize, + on_chunk: Option, + on_complete: Option)>) + 'static>, +) { + socket_connection.input_stream().read_bytes_async( + bytes_in_chunk, + priority, + cancelable.clone().as_ref(), + move |result| match result { + Ok(bytes) => { + // Update bytes total + let bytes_total = bytes_total + bytes.len(); + + // Callback chunk function + if let Some(ref callback) = on_chunk { + callback((&bytes, bytes_total)); + } + + // Validate max size + if bytes_total > bytes_total_limit { + if let Some(callback) = on_complete { + callback(Err((Error::BytesTotal, None))); + } + return; // break + } + + // No bytes were read, end of stream + if bytes.len() == 0 { + if let Some(callback) = on_complete { + callback(Ok(memory_input_stream)); + } + return; // break + } + + // Write chunk bytes + memory_input_stream.add_bytes(&bytes); + + // Continue + read_all_from_socket_connection_async( + memory_input_stream, + socket_connection, + cancelable, + priority, + bytes_in_chunk, + bytes_total_limit, + bytes_total, + on_chunk, + on_complete, + ); + } + Err(reason) => { + if let Some(callback) = on_complete { + callback(Err((Error::InputStream, Some(reason.message())))); + } + } + }, + ); +} diff --git a/src/gio/memory_input_stream/error.rs b/src/gio/memory_input_stream/error.rs new file mode 100644 index 0000000..093d2a2 --- /dev/null +++ b/src/gio/memory_input_stream/error.rs @@ -0,0 +1,4 @@ +pub enum Error { + BytesTotal, + InputStream, +} diff --git a/src/lib.rs b/src/lib.rs index b9babe5..26403ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ pub mod client; +pub mod gio; From 4ab83702e9bb76110a87c8eef901287f5fc07c5f Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 29 Oct 2024 16:33:32 +0200 Subject: [PATCH 061/392] use reference without copy usize --- src/gio/memory_input_stream.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 202c3a2..46d8b6e 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -34,7 +34,7 @@ pub fn from_socket_connection_async( priority: Priority, bytes_in_chunk: usize, bytes_total_limit: usize, - on_chunk: Option, + on_chunk: Option, on_complete: Option)>) + 'static>, ) { read_all_from_socket_connection_async( @@ -82,7 +82,7 @@ pub fn read_all_from_socket_connection_async( bytes_in_chunk: usize, bytes_total_limit: usize, bytes_total: usize, - on_chunk: Option, + on_chunk: Option, on_complete: Option)>) + 'static>, ) { socket_connection.input_stream().read_bytes_async( @@ -96,7 +96,7 @@ pub fn read_all_from_socket_connection_async( // Callback chunk function if let Some(ref callback) = on_chunk { - callback((&bytes, bytes_total)); + callback((&bytes, &bytes_total)); } // Validate max size From 3e8da1725c4e35e9d0ebac481c9a153ca4ce85ec Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 29 Oct 2024 16:51:05 +0200 Subject: [PATCH 062/392] remove option wrapper from callbacks --- src/gio/memory_input_stream.rs | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 46d8b6e..7149df4 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -25,7 +25,7 @@ use glib::{Bytes, Priority}; /// * recursively read all bytes from `InputStream` for `SocketConnection` according to `bytes_in_chunk` argument /// * calculates total bytes length on every chunk iteration, validate sum with `bytes_total_limit` argument /// * stop reading `InputStream` with `Result` on zero bytes in chunk received -/// * applies optional callback functions: +/// * applies callback functions: /// * `on_chunk` - return reference to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) and `bytes_total` collected for every chunk in reading loop /// * `on_complete` - return `MemoryInputStream` on success or `Error` on failure as `Result` pub fn from_socket_connection_async( @@ -34,8 +34,8 @@ pub fn from_socket_connection_async( priority: Priority, bytes_in_chunk: usize, bytes_total_limit: usize, - on_chunk: Option, - on_complete: Option)>) + 'static>, + on_chunk: impl Fn((&Bytes, &usize)) + 'static, + on_complete: impl FnOnce(Result)>) + 'static, ) { read_all_from_socket_connection_async( MemoryInputStream::new(), @@ -56,7 +56,6 @@ pub fn from_socket_connection_async( /// Asynchronously read [InputStream](https://docs.gtk.org/gio/class.InputStream.html) /// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// to given [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html). -/// Applies optional `on_chunk` and `on_complete` callback functions. /// /// Useful to create dynamically allocated, memory-safe buffer /// from remote connections, where final size of target data could not be known by Gemini protocol restrictions. @@ -71,7 +70,7 @@ pub fn from_socket_connection_async( /// * recursively read all bytes from `InputStream` for `SocketConnection` according to `bytes_in_chunk` argument /// * calculates total bytes length on every chunk iteration, validate sum with `bytes_total_limit` argument /// * stop reading `InputStream` with `Result` on zero bytes in chunk received, otherwise continue next chunk request in loop -/// * applies optional callback functions: +/// * applies callback functions: /// * `on_chunk` - return reference to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) and `bytes_total` collected for every chunk in reading loop /// * `on_complete` - return `MemoryInputStream` on success or `Error` on failure as `Result` pub fn read_all_from_socket_connection_async( @@ -82,8 +81,8 @@ pub fn read_all_from_socket_connection_async( bytes_in_chunk: usize, bytes_total_limit: usize, bytes_total: usize, - on_chunk: Option, - on_complete: Option)>) + 'static>, + on_chunk: impl Fn((&Bytes, &usize)) + 'static, + on_complete: impl FnOnce(Result)>) + 'static, ) { socket_connection.input_stream().read_bytes_async( bytes_in_chunk, @@ -95,24 +94,16 @@ pub fn read_all_from_socket_connection_async( let bytes_total = bytes_total + bytes.len(); // Callback chunk function - if let Some(ref callback) = on_chunk { - callback((&bytes, &bytes_total)); - } + on_chunk((&bytes, &bytes_total)); // Validate max size if bytes_total > bytes_total_limit { - if let Some(callback) = on_complete { - callback(Err((Error::BytesTotal, None))); - } - return; // break + return on_complete(Err((Error::BytesTotal, None))); } // No bytes were read, end of stream if bytes.len() == 0 { - if let Some(callback) = on_complete { - callback(Ok(memory_input_stream)); - } - return; // break + return on_complete(Ok(memory_input_stream)); } // Write chunk bytes @@ -132,9 +123,7 @@ pub fn read_all_from_socket_connection_async( ); } Err(reason) => { - if let Some(callback) = on_complete { - callback(Err((Error::InputStream, Some(reason.message())))); - } + on_complete(Err((Error::InputStream, Some(reason.message())))); } }, ); From 028ffa384d086bf7d3d67a34fa6c6923a02333bc Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 29 Oct 2024 17:06:48 +0200 Subject: [PATCH 063/392] derive debug --- src/gio/memory_input_stream/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gio/memory_input_stream/error.rs b/src/gio/memory_input_stream/error.rs index 093d2a2..b5cda40 100644 --- a/src/gio/memory_input_stream/error.rs +++ b/src/gio/memory_input_stream/error.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub enum Error { BytesTotal, InputStream, From 268dab6ed36047b401f4d392fd27597321514e3a Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 29 Oct 2024 21:19:49 +0200 Subject: [PATCH 064/392] use clone of shared reference instead of ref --- src/gio/memory_input_stream.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 7149df4..1c8d193 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -34,7 +34,7 @@ pub fn from_socket_connection_async( priority: Priority, bytes_in_chunk: usize, bytes_total_limit: usize, - on_chunk: impl Fn((&Bytes, &usize)) + 'static, + on_chunk: impl Fn((Bytes, usize)) + 'static, on_complete: impl FnOnce(Result)>) + 'static, ) { read_all_from_socket_connection_async( @@ -81,7 +81,7 @@ pub fn read_all_from_socket_connection_async( bytes_in_chunk: usize, bytes_total_limit: usize, bytes_total: usize, - on_chunk: impl Fn((&Bytes, &usize)) + 'static, + on_chunk: impl Fn((Bytes, usize)) + 'static, on_complete: impl FnOnce(Result)>) + 'static, ) { socket_connection.input_stream().read_bytes_async( @@ -94,7 +94,7 @@ pub fn read_all_from_socket_connection_async( let bytes_total = bytes_total + bytes.len(); // Callback chunk function - on_chunk((&bytes, &bytes_total)); + on_chunk((bytes.clone(), bytes_total)); // Validate max size if bytes_total > bytes_total_limit { From 8f7bbaec761914901b718bf4243a6853738c5e99 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 04:31:26 +0200 Subject: [PATCH 065/392] add Protocol error --- src/client/response/header/mime.rs | 2 +- src/client/response/header/mime/error.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/response/header/mime.rs b/src/client/response/header/mime.rs index 73044d9..d7ea2d0 100644 --- a/src/client/response/header/mime.rs +++ b/src/client/response/header/mime.rs @@ -28,7 +28,7 @@ impl Mime { Ok(string) => Self::from_string(string.as_str()), Err(_) => Err(Error::Decode), }, - None => Err(Error::Undefined), + None => Err(Error::Protocol), } } diff --git a/src/client/response/header/mime/error.rs b/src/client/response/header/mime/error.rs index d9b19fd..989e734 100644 --- a/src/client/response/header/mime/error.rs +++ b/src/client/response/header/mime/error.rs @@ -1,5 +1,6 @@ #[derive(Debug)] pub enum Error { Decode, + Protocol, Undefined, } From c2b06fd6888f417be9dc317fe4c1e661bd241396 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 04:35:04 +0200 Subject: [PATCH 066/392] fix status code error types --- src/client/response/header/meta.rs | 4 ++-- src/client/response/header/meta/error.rs | 1 + src/client/response/header/status.rs | 2 +- src/client/response/header/status/error.rs | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index 17bf70e..b7af3e2 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -16,14 +16,14 @@ impl Meta { Some(value) => Ok(Self { buffer: value.to_vec(), }), - None => return Err(Error::Undefined), + None => return Err(Error::Protocol), } } pub fn to_gstring(&self) -> Result { match GString::from_utf8(self.buffer.clone()) { Ok(result) => Ok(result), - Err(_) => Err(Error::Undefined), + Err(_) => Err(Error::Decode), } } diff --git a/src/client/response/header/meta/error.rs b/src/client/response/header/meta/error.rs index d9b19fd..989e734 100644 --- a/src/client/response/header/meta/error.rs +++ b/src/client/response/header/meta/error.rs @@ -1,5 +1,6 @@ #[derive(Debug)] pub enum Error { Decode, + Protocol, Undefined, } diff --git a/src/client/response/header/status.rs b/src/client/response/header/status.rs index 6097171..e24cbca 100644 --- a/src/client/response/header/status.rs +++ b/src/client/response/header/status.rs @@ -23,7 +23,7 @@ impl Status { Ok(string) => Self::from_string(string.as_str()), Err(_) => Err(Error::Decode), }, - None => Err(Error::Undefined), + None => Err(Error::Protocol), } } diff --git a/src/client/response/header/status/error.rs b/src/client/response/header/status/error.rs index d9b19fd..989e734 100644 --- a/src/client/response/header/status/error.rs +++ b/src/client/response/header/status/error.rs @@ -1,5 +1,6 @@ #[derive(Debug)] pub enum Error { Decode, + Protocol, Undefined, } From 8dcab0f29e26dea6f477b59006f691d4465f3061 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 04:37:05 +0200 Subject: [PATCH 067/392] add new enums --- src/client/response/header.rs | 1 + src/client/response/header/error.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/client/response/header.rs b/src/client/response/header.rs index 1b73346..a415d80 100644 --- a/src/client/response/header.rs +++ b/src/client/response/header.rs @@ -66,6 +66,7 @@ impl Header { match reason { status::Error::Decode => Error::StatusDecode, status::Error::Undefined => Error::StatusUndefined, + status::Error::Protocol => Error::StatusProtocol, }, None, )), diff --git a/src/client/response/header/error.rs b/src/client/response/header/error.rs index e8176bd..17ff132 100644 --- a/src/client/response/header/error.rs +++ b/src/client/response/header/error.rs @@ -4,5 +4,6 @@ pub enum Error { InputStream, Protocol, StatusDecode, + StatusProtocol, StatusUndefined, } From 2331bf8e27aff9400cd3a5bff30af4060ae3829a Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 04:40:20 +0200 Subject: [PATCH 068/392] fix typo --- src/client/response/header/meta.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index b7af3e2..a504eb0 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -5,7 +5,7 @@ use glib::GString; /// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). /// -/// Usefult to grab placeholder text on 10, 11, 31 codes processing +/// Useful to grab placeholder text on 10, 11, 31 codes processing pub struct Meta { buffer: Vec, } From ebfdf6a1d0c088432b0cfb29e4d0fecb41bf7c5c Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 04:47:59 +0200 Subject: [PATCH 069/392] update comment --- src/client/response/header/meta.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index a504eb0..58afd0f 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -5,7 +5,9 @@ use glib::GString; /// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). /// -/// Useful to grab placeholder text on 10, 11, 31 codes processing +/// Use as: +/// * placeholder value for 10, 11 +/// * URL for 30, 31 pub struct Meta { buffer: Vec, } @@ -16,7 +18,7 @@ impl Meta { Some(value) => Ok(Self { buffer: value.to_vec(), }), - None => return Err(Error::Protocol), + None => return Err(Error::Protocol), // @TODO optional } } From daa84b6a6b8d98a99c31229f56a340412bafaedf Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 04:49:50 +0200 Subject: [PATCH 070/392] update comment --- src/client/response/header/meta.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index 58afd0f..5897908 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -3,11 +3,11 @@ pub use error::Error; use glib::GString; -/// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). +/// Response meta holder, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). /// /// Use as: -/// * placeholder value for 10, 11 -/// * URL for 30, 31 +/// * placeholder for 10, 11 status +/// * URL for 30, 31 status pub struct Meta { buffer: Vec, } From f8fa101c5994626deccb95a19f30c80177043aca Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 04:50:27 +0200 Subject: [PATCH 071/392] update comment --- src/client/response/header/meta.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index 5897908..4cded7d 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -3,7 +3,7 @@ pub use error::Error; use glib::GString; -/// Response meta holder, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). +/// Response meta holder /// /// Use as: /// * placeholder for 10, 11 status From 8c1afd654a157ec8f25af7e21344dceaf5f99616 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 05:41:25 +0200 Subject: [PATCH 072/392] update meta parser --- src/client/response/header.rs | 6 ++-- src/client/response/header/meta.rs | 53 +++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/client/response/header.rs b/src/client/response/header.rs index a415d80..969696d 100644 --- a/src/client/response/header.rs +++ b/src/client/response/header.rs @@ -53,13 +53,13 @@ impl Header { match Status::from_header(&buffer) { Ok(status) => Ok(Self { status, - meta: match Meta::from_header(&buffer) { + meta: match Meta::from(&buffer) { Ok(meta) => Some(meta), - Err(_) => None, + Err(_) => None, // @TODO handle }, mime: match Mime::from_header(&buffer) { Ok(mime) => Some(mime), - Err(_) => None, + Err(_) => None, // @TODO handle }, }), Err(reason) => Err(( diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index 4cded7d..c82f61e 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -9,27 +9,50 @@ use glib::GString; /// * placeholder for 10, 11 status /// * URL for 30, 31 status pub struct Meta { - buffer: Vec, + value: Option, } impl Meta { - pub fn from_header(buffer: &[u8]) -> Result { + /// Parse Meta from UTF-8 + pub fn from(buffer: &[u8]) -> Result { + // Init bytes buffer + let mut bytes: Vec = Vec::new(); + + // Skip status code match buffer.get(3..) { - Some(value) => Ok(Self { - buffer: value.to_vec(), - }), - None => return Err(Error::Protocol), // @TODO optional + Some(slice) => { + for (count, &byte) in slice.iter().enumerate() { + // Validate length + if count > 0x400 { + // 1024 + return Err(Error::Protocol); + } + + // End of line, done + if byte == b'\r' { + break; + } + + // Append + bytes.push(byte); + } + + // Assumes the bytes are valid UTF-8 + match GString::from_utf8(bytes) { + Ok(value) => Ok(Self { + value: match value.is_empty() { + true => None, + false => Some(value), + }, + }), + Err(_) => Err(Error::Decode), + } + } + None => Err(Error::Protocol), } } - pub fn to_gstring(&self) -> Result { - match GString::from_utf8(self.buffer.clone()) { - Ok(result) => Ok(result), - Err(_) => Err(Error::Decode), - } - } - - pub fn buffer(&self) -> &[u8] { - &self.buffer + pub fn value(&self) -> &Option { + &self.value } } From 36b8342f29c4550f8b6128339d8f637786d331ae Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 14:22:08 +0200 Subject: [PATCH 073/392] update meta parser --- src/client/response/header/meta.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index c82f61e..0820871 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -5,6 +5,8 @@ use glib::GString; /// Response meta holder /// +/// Could be created from entire response buffer or just header slice +/// /// Use as: /// * placeholder for 10, 11 status /// * URL for 30, 31 status @@ -18,22 +20,16 @@ impl Meta { // Init bytes buffer let mut bytes: Vec = Vec::new(); - // Skip status code - match buffer.get(3..) { + // Skip 3 bytes for status code of 1024 expected + match buffer.get(3..1021) { Some(slice) => { - for (count, &byte) in slice.iter().enumerate() { - // Validate length - if count > 0x400 { - // 1024 - return Err(Error::Protocol); - } - - // End of line, done + for &byte in slice { + // End of header if byte == b'\r' { break; } - // Append + // Continue bytes.push(byte); } From 47f58f800d1bc84bd1e2df4a2c7f5b8c2812211c Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 14:23:40 +0200 Subject: [PATCH 074/392] add initial capacity --- src/client/response/header/meta.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/response/header/meta.rs b/src/client/response/header/meta.rs index 0820871..e086d5f 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/header/meta.rs @@ -18,7 +18,7 @@ impl Meta { /// Parse Meta from UTF-8 pub fn from(buffer: &[u8]) -> Result { // Init bytes buffer - let mut bytes: Vec = Vec::new(); + let mut bytes: Vec = Vec::with_capacity(1021); // Skip 3 bytes for status code of 1024 expected match buffer.get(3..1021) { From 93985095a51923563b3ecdb2057de23cef0e5de1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 18:28:20 +0200 Subject: [PATCH 075/392] update api version with new implementation --- Cargo.toml | 2 +- README.md | 12 +- src/client/response.rs | 4 +- src/client/response/header.rs | 151 -------------- src/client/response/meta.rs | 191 ++++++++++++++++++ .../response/{header => meta}/charset.rs | 0 .../response/{header/meta.rs => meta/data.rs} | 21 +- .../{header/status => meta/data}/error.rs | 1 - src/client/response/{header => meta}/error.rs | 6 +- .../response/{header => meta}/language.rs | 0 src/client/response/{header => meta}/mime.rs | 9 +- .../{header/meta => meta/mime}/error.rs | 0 .../response/{header => meta}/status.rs | 2 +- .../{header/mime => meta/status}/error.rs | 0 14 files changed, 221 insertions(+), 178 deletions(-) delete mode 100644 src/client/response/header.rs create mode 100644 src/client/response/meta.rs rename src/client/response/{header => meta}/charset.rs (100%) rename src/client/response/{header/meta.rs => meta/data.rs} (72%) rename src/client/response/{header/status => meta/data}/error.rs (80%) rename src/client/response/{header => meta}/error.rs (59%) rename src/client/response/{header => meta}/language.rs (100%) rename src/client/response/{header => meta}/mime.rs (89%) rename src/client/response/{header/meta => meta/mime}/error.rs (100%) rename src/client/response/{header => meta}/status.rs (93%) rename src/client/response/{header/mime => meta/status}/error.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index d5d0ab7..e1ae956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.5.1" +version = "0.6.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index bcc4a0d..e2cf971 100644 --- a/README.md +++ b/README.md @@ -17,21 +17,13 @@ cargo add ggemini ## Usage +* [Documentation](https://docs.rs/ggemini/latest/ggemini/) + _todo_ ### `client` - -[Gio](https://docs.gtk.org/gio/) API already provides powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html)\ -`client` collection just extends some features wanted for Gemini Protocol interaction. - -#### `client::response` -#### `client::response::header` -#### `client::response::body` - ### `gio` -#### `gio::memory_input_stream` - ## See also * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file diff --git a/src/client/response.rs b/src/client/response.rs index a6016ba..cc98b14 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,5 +1,5 @@ pub mod body; -pub mod header; +pub mod meta; pub use body::Body; -pub use header::Header; +pub use meta::Meta; diff --git a/src/client/response/header.rs b/src/client/response/header.rs deleted file mode 100644 index 969696d..0000000 --- a/src/client/response/header.rs +++ /dev/null @@ -1,151 +0,0 @@ -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; - -use gio::{ - prelude::{IOStreamExt, InputStreamExt}, - Cancellable, SocketConnection, -}; -use glib::{Bytes, Priority}; - -pub const HEADER_BYTES_LEN: usize = 0x400; // 1024 - -pub struct Header { - status: Status, - meta: Option, - mime: Option, - // @TODO - // charset: Option, - // language: Option, -} - -impl Header { - // Constructors - - pub fn from_socket_connection_async( - socket_connection: SocketConnection, - priority: Option, - cancellable: Option, - callback: impl FnOnce(Result)>) + 'static, - ) { - // Take header buffer from input stream - Self::read_from_socket_connection_async( - Vec::with_capacity(HEADER_BYTES_LEN), - socket_connection, - match cancellable { - Some(value) => Some(value), - None => None::, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - |result| { - callback(match result { - Ok(buffer) => { - // Status is required, parse to continue - match Status::from_header(&buffer) { - Ok(status) => Ok(Self { - status, - meta: match Meta::from(&buffer) { - Ok(meta) => Some(meta), - Err(_) => None, // @TODO handle - }, - mime: match Mime::from_header(&buffer) { - Ok(mime) => Some(mime), - Err(_) => None, // @TODO handle - }, - }), - Err(reason) => Err(( - match reason { - status::Error::Decode => Error::StatusDecode, - status::Error::Undefined => Error::StatusUndefined, - status::Error::Protocol => Error::StatusProtocol, - }, - None, - )), - } - } - Err(error) => Err(error), - }) - }, - ); - } - - // Getters - - pub fn status(&self) -> &Status { - &self.status - } - - pub fn mime(&self) -> &Option { - &self.mime - } - - pub fn meta(&self) -> &Option { - &self.meta - } - - // Tools - - pub fn read_from_socket_connection_async( - mut buffer: Vec, - connection: SocketConnection, - cancellable: Option, - priority: Priority, - callback: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, - ) { - connection.input_stream().read_bytes_async( - 1, // do not change! - priority, - cancellable.clone().as_ref(), - move |result| match result { - Ok(bytes) => { - // Expect valid header length - if bytes.len() == 0 || buffer.len() >= HEADER_BYTES_LEN { - return callback(Err((Error::Protocol, None))); - } - - // Read next byte without buffer record - if bytes.contains(&b'\r') { - return Self::read_from_socket_connection_async( - buffer, - connection, - cancellable, - priority, - callback, - ); - } - - // Complete without buffer record - if bytes.contains(&b'\n') { - return callback(Ok(buffer - .iter() - .flat_map(|byte| byte.iter()) - .cloned() - .collect())); // convert to UTF-8 - } - - // Record - buffer.push(bytes); - - // Continue - Self::read_from_socket_connection_async( - buffer, - connection, - cancellable, - priority, - callback, - ); - } - Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), - }, - ); - } -} diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs new file mode 100644 index 0000000..2143b64 --- /dev/null +++ b/src/client/response/meta.rs @@ -0,0 +1,191 @@ +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, InputStreamExt}, + Cancellable, SocketConnection, +}; +use glib::{Bytes, Priority}; + +pub const MAX_LEN: usize = 0x400; // 1024 + +pub struct Meta { + data: Data, + mime: Mime, + status: Status, + // @TODO + // charset: Charset, + // language: Language, +} + +impl Meta { + // Constructors + + /// Create new `Self` from UTF-8 buffer + pub fn from_utf8(buffer: &[u8]) -> Result)> { + let len = buffer.len(); + + match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { + Some(slice) => { + // Parse data + let data = Data::from_utf8(&slice); + + if let Err(reason) = data { + return Err(( + match reason { + data::Error::Decode => Error::DataDecode, + data::Error::Protocol => Error::DataProtocol, + }, + None, + )); + } + + // MIME + + let mime = Mime::from_utf8(&slice); + + if let Err(reason) = mime { + return Err(( + match reason { + mime::Error::Decode => Error::MimeDecode, + mime::Error::Protocol => Error::MimeProtocol, + mime::Error::Undefined => Error::MimeUndefined, + }, + None, + )); + } + + // Status + + let status = Status::from_utf8(&slice); + + if let Err(reason) = status { + return Err(( + match reason { + status::Error::Decode => Error::StatusDecode, + status::Error::Protocol => Error::StatusProtocol, + status::Error::Undefined => Error::StatusUndefined, + }, + None, + )); + } + + Ok(Self { + data: data.unwrap(), + mime: mime.unwrap(), + status: status.unwrap(), + }) + } + None => Err((Error::Protocol, None)), + } + } + + /// Asynchronously create new `Self` from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) + /// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + pub fn from_socket_connection_async( + socket_connection: SocketConnection, + priority: Option, + cancellable: Option, + on_complete: impl FnOnce(Result)>) + 'static, + ) { + read_from_socket_connection_async( + Vec::with_capacity(MAX_LEN), + socket_connection, + match cancellable { + Some(value) => Some(value), + None => None::, + }, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + |result| match result { + Ok(buffer) => on_complete(Self::from_utf8(&buffer)), + Err(reason) => on_complete(Err(reason)), + }, + ); + } + + // Getters + + pub fn status(&self) -> &Status { + &self.status + } + + pub fn data(&self) -> &Data { + &self.data + } + + pub fn mime(&self) -> &Mime { + &self.mime + } +} + +// Tools + +/// Asynchronously take meta bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// +/// * this function implements low-level helper for `Meta::from_socket_connection_async`, also provides public API for external integrations +/// * requires entire `SocketConnection` instead of `InputStream` to keep connection alive in async context +pub fn read_from_socket_connection_async( + mut buffer: Vec, + connection: SocketConnection, + cancellable: Option, + priority: Priority, + on_complete: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, +) { + connection.input_stream().read_bytes_async( + 1, // do not change! + priority, + cancellable.clone().as_ref(), + move |result| match result { + Ok(bytes) => { + // Expect valid header length + if bytes.len() == 0 || buffer.len() >= MAX_LEN { + return on_complete(Err((Error::Protocol, None))); + } + + // Read next byte without buffer record + if bytes.contains(&b'\r') { + return read_from_socket_connection_async( + buffer, + connection, + cancellable, + priority, + on_complete, + ); + } + + // Complete without buffer record + if bytes.contains(&b'\n') { + return on_complete(Ok(buffer + .iter() + .flat_map(|byte| byte.iter()) + .cloned() + .collect())); // convert to UTF-8 + } + + // Record + buffer.push(bytes); + + // Continue + read_from_socket_connection_async( + buffer, + connection, + cancellable, + priority, + on_complete, + ); + } + Err(reason) => on_complete(Err((Error::InputStream, Some(reason.message())))), + }, + ); +} diff --git a/src/client/response/header/charset.rs b/src/client/response/meta/charset.rs similarity index 100% rename from src/client/response/header/charset.rs rename to src/client/response/meta/charset.rs diff --git a/src/client/response/header/meta.rs b/src/client/response/meta/data.rs similarity index 72% rename from src/client/response/header/meta.rs rename to src/client/response/meta/data.rs index e086d5f..d903f08 100644 --- a/src/client/response/header/meta.rs +++ b/src/client/response/meta/data.rs @@ -3,25 +3,30 @@ pub use error::Error; use glib::GString; -/// Response meta holder +pub const MAX_LEN: usize = 0x400; // 1024 + +/// Meta data holder for response /// /// Could be created from entire response buffer or just header slice /// /// Use as: /// * placeholder for 10, 11 status /// * URL for 30, 31 status -pub struct Meta { +pub struct Data { value: Option, } -impl Meta { +impl Data { /// Parse Meta from UTF-8 - pub fn from(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { // Init bytes buffer - let mut bytes: Vec = Vec::with_capacity(1021); + let mut bytes: Vec = Vec::with_capacity(MAX_LEN); - // Skip 3 bytes for status code of 1024 expected - match buffer.get(3..1021) { + // 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 @@ -37,8 +42,8 @@ impl Meta { match GString::from_utf8(bytes) { Ok(value) => Ok(Self { value: match value.is_empty() { - true => None, false => Some(value), + true => None, }, }), Err(_) => Err(Error::Decode), diff --git a/src/client/response/header/status/error.rs b/src/client/response/meta/data/error.rs similarity index 80% rename from src/client/response/header/status/error.rs rename to src/client/response/meta/data/error.rs index 989e734..125f9c6 100644 --- a/src/client/response/header/status/error.rs +++ b/src/client/response/meta/data/error.rs @@ -2,5 +2,4 @@ pub enum Error { Decode, Protocol, - Undefined, } diff --git a/src/client/response/header/error.rs b/src/client/response/meta/error.rs similarity index 59% rename from src/client/response/header/error.rs rename to src/client/response/meta/error.rs index 17ff132..b1414eb 100644 --- a/src/client/response/header/error.rs +++ b/src/client/response/meta/error.rs @@ -1,7 +1,11 @@ #[derive(Debug)] pub enum Error { - Buffer, + DataDecode, + DataProtocol, InputStream, + MimeDecode, + MimeProtocol, + MimeUndefined, Protocol, StatusDecode, StatusProtocol, diff --git a/src/client/response/header/language.rs b/src/client/response/meta/language.rs similarity index 100% rename from src/client/response/header/language.rs rename to src/client/response/meta/language.rs diff --git a/src/client/response/header/mime.rs b/src/client/response/meta/mime.rs similarity index 89% rename from src/client/response/header/mime.rs rename to src/client/response/meta/mime.rs index d7ea2d0..82e6c14 100644 --- a/src/client/response/header/mime.rs +++ b/src/client/response/meta/mime.rs @@ -4,6 +4,8 @@ pub use error::Error; use glib::{GString, Uri}; use std::path::Path; +pub const MAX_LEN: usize = 0x400; // 1024 + /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters #[derive(Debug)] pub enum Mime { @@ -22,9 +24,10 @@ pub enum Mime { } // @TODO impl Mime { - pub fn from_header(buffer: &[u8]) -> Result { - match buffer.get(..) { - Some(value) => match GString::from_utf8(value.to_vec()) { + pub fn from_utf8(buffer: &[u8]) -> Result { + let len = buffer.len(); + 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(_) => Err(Error::Decode), }, diff --git a/src/client/response/header/meta/error.rs b/src/client/response/meta/mime/error.rs similarity index 100% rename from src/client/response/header/meta/error.rs rename to src/client/response/meta/mime/error.rs diff --git a/src/client/response/header/status.rs b/src/client/response/meta/status.rs similarity index 93% rename from src/client/response/header/status.rs rename to src/client/response/meta/status.rs index e24cbca..2875d31 100644 --- a/src/client/response/header/status.rs +++ b/src/client/response/meta/status.rs @@ -17,7 +17,7 @@ pub enum Status { } // @TODO impl Status { - pub fn from_header(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.get(0..2) { Some(value) => match GString::from_utf8(value.to_vec()) { Ok(string) => Self::from_string(string.as_str()), diff --git a/src/client/response/header/mime/error.rs b/src/client/response/meta/status/error.rs similarity index 100% rename from src/client/response/header/mime/error.rs rename to src/client/response/meta/status/error.rs From c9209b17431a65841d90f13732cf313c5b40db4d Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 30 Oct 2024 23:52:25 +0200 Subject: [PATCH 076/392] remove extra conversions --- src/client/response/meta.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 2143b64..6ad13c3 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -9,10 +9,10 @@ pub use mime::Mime; pub use status::Status; use gio::{ - prelude::{IOStreamExt, InputStreamExt}, + prelude::{IOStreamExt, InputStreamExtManual}, Cancellable, SocketConnection, }; -use glib::{Bytes, Priority}; +use glib::Priority; pub const MAX_LEN: usize = 0x400; // 1024 @@ -133,23 +133,25 @@ impl Meta { /// Asynchronously take meta bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) /// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// +/// Return UTF-8 buffer collected. +/// /// * this function implements low-level helper for `Meta::from_socket_connection_async`, also provides public API for external integrations /// * requires entire `SocketConnection` instead of `InputStream` to keep connection alive in async context pub fn read_from_socket_connection_async( - mut buffer: Vec, + mut buffer: Vec, connection: SocketConnection, cancellable: Option, priority: Priority, on_complete: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, ) { - connection.input_stream().read_bytes_async( - 1, // do not change! + connection.input_stream().read_async( + vec![0], priority, cancellable.clone().as_ref(), move |result| match result { - Ok(bytes) => { + Ok((mut bytes, size)) => { // Expect valid header length - if bytes.len() == 0 || buffer.len() >= MAX_LEN { + if size == 0 || buffer.len() >= MAX_LEN { return on_complete(Err((Error::Protocol, None))); } @@ -166,15 +168,11 @@ pub fn read_from_socket_connection_async( // Complete without buffer record if bytes.contains(&b'\n') { - return on_complete(Ok(buffer - .iter() - .flat_map(|byte| byte.iter()) - .cloned() - .collect())); // convert to UTF-8 + return on_complete(Ok(buffer)); } // Record - buffer.push(bytes); + buffer.append(&mut bytes); // Continue read_from_socket_connection_async( @@ -185,7 +183,7 @@ pub fn read_from_socket_connection_async( on_complete, ); } - Err(reason) => on_complete(Err((Error::InputStream, Some(reason.message())))), + Err((_, reason)) => on_complete(Err((Error::InputStream, Some(reason.message())))), }, ); } From 53473c38fea95d1c114d3d4b983f28a653be793f Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 00:59:51 +0200 Subject: [PATCH 077/392] update comments --- src/client/response/meta.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 6ad13c3..9691f23 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -130,7 +130,7 @@ impl Meta { // Tools -/// Asynchronously take meta bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +/// Asynchronously read all meta bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) /// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// /// Return UTF-8 buffer collected. @@ -155,7 +155,7 @@ pub fn read_from_socket_connection_async( return on_complete(Err((Error::Protocol, None))); } - // Read next byte without buffer record + // Read next byte without record if bytes.contains(&b'\r') { return read_from_socket_connection_async( buffer, @@ -166,7 +166,7 @@ pub fn read_from_socket_connection_async( ); } - // Complete without buffer record + // Complete without record if bytes.contains(&b'\n') { return on_complete(Ok(buffer)); } From 99d7baff58d4a9859bee917b89f14b03d1cbd10a Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 01:00:04 +0200 Subject: [PATCH 078/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e1ae956..a0b883d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.6.0" +version = "0.7.0" edition = "2021" license = "MIT" readme = "README.md" From c1bae6e6b5f7b56b068fde01963b1ef1d5cfab08 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 02:31:10 +0200 Subject: [PATCH 079/392] add comment --- src/client/response/meta.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 9691f23..a7ad1c7 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -32,6 +32,7 @@ impl Meta { pub fn from_utf8(buffer: &[u8]) -> Result)> { let len = buffer.len(); + // Can parse from entire response or just meta buffer given match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { Some(slice) => { // Parse data From 4188842eb5d4f4de84282de92d75a627de306728 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 02:39:22 +0200 Subject: [PATCH 080/392] update comment --- src/client/response/meta.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index a7ad1c7..5923ee6 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -137,7 +137,7 @@ impl Meta { /// Return UTF-8 buffer collected. /// /// * this function implements low-level helper for `Meta::from_socket_connection_async`, also provides public API for external integrations -/// * requires entire `SocketConnection` instead of `InputStream` to keep connection alive in async context +/// * requires `SocketConnection` instead of `InputStream` to keep connection alive (by increasing reference count in async context) @TODO pub fn read_from_socket_connection_async( mut buffer: Vec, connection: SocketConnection, From 06345dedaf3ba1ca5de0ed800b98a82f82b1512b Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 03:11:08 +0200 Subject: [PATCH 081/392] add comment --- src/client/response/meta.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 5923ee6..fb14f4e 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -1,3 +1,8 @@ +//! 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; From 63a9874dcfd0c0c99524783ada800b9ebc3c291e Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 03:14:52 +0200 Subject: [PATCH 082/392] add comment --- src/client.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client.rs b/src/client.rs index 4c6f2cd..1f2686b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1 +1,4 @@ +//! Client API to interact Server for +//! [Gemini protocol](https://geminiprotocol.net/docs/protocol-specification.gmi) + pub mod response; From b73a9388eeca89eb606d5b604625d08536337aa3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 03:17:57 +0200 Subject: [PATCH 083/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 1f2686b..7335b61 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,4 @@ -//! Client API to interact Server for +//! Client API to interact Server using //! [Gemini protocol](https://geminiprotocol.net/docs/protocol-specification.gmi) pub mod response; From 394ae0cf4f7c8b73088345bb5de50862b4d7cf66 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 03:22:27 +0200 Subject: [PATCH 084/392] update comment --- src/client/response.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/response.rs b/src/client/response.rs index cc98b14..b75730f 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,3 +1,5 @@ +//! Read and parse Gemini response as GObject + pub mod body; pub mod meta; From d5cea24891f3ee47dfe27e270f4e513e586acae3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 03:24:41 +0200 Subject: [PATCH 085/392] draft new components --- src/client/response.rs | 1 + src/client/response/data.rs | 6 ++++++ src/client/response/data/text/error.rs | 4 ++++ 3 files changed, 11 insertions(+) create mode 100644 src/client/response/data.rs create mode 100644 src/client/response/data/text/error.rs diff --git a/src/client/response.rs b/src/client/response.rs index b75730f..fdfb6a8 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,6 +1,7 @@ //! Read and parse Gemini response as GObject pub mod body; +pub mod data; pub mod meta; pub use body::Body; diff --git a/src/client/response/data.rs b/src/client/response/data.rs new file mode 100644 index 0000000..ca43b82 --- /dev/null +++ b/src/client/response/data.rs @@ -0,0 +1,6 @@ +//! Gemini response could have different MIME type for data. +//! Use one of these components to parse response according to content type expected. +//! +//! * MIME type could be detected using `client::response::Meta` parser + +pub mod text; diff --git a/src/client/response/data/text/error.rs b/src/client/response/data/text/error.rs new file mode 100644 index 0000000..8b32c89 --- /dev/null +++ b/src/client/response/data/text/error.rs @@ -0,0 +1,4 @@ +#[derive(Debug)] +pub enum Error { + // nothing yet.. +} From 20270d6b308c72a1684935cfc5ecfd904eefa6cd Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 05:17:02 +0200 Subject: [PATCH 086/392] move body component to data::text namespace --- src/client/response.rs | 4 +- src/client/response/body.rs | 200 ------------------------- src/client/response/body/error.rs | 5 - src/client/response/data.rs | 3 +- src/client/response/data/text.rs | 121 +++++++++++++++ src/client/response/data/text/error.rs | 4 +- 6 files changed, 127 insertions(+), 210 deletions(-) delete mode 100644 src/client/response/body.rs delete mode 100644 src/client/response/body/error.rs create mode 100644 src/client/response/data/text.rs diff --git a/src/client/response.rs b/src/client/response.rs index fdfb6a8..e9ecdf2 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,8 +1,6 @@ -//! Read and parse Gemini response as GObject +//! Read and parse Gemini response as Object -pub mod body; pub mod data; pub mod meta; -pub use body::Body; pub use meta::Meta; diff --git a/src/client/response/body.rs b/src/client/response/body.rs deleted file mode 100644 index a00eda3..0000000 --- a/src/client/response/body.rs +++ /dev/null @@ -1,200 +0,0 @@ -pub mod error; -pub use error::Error; - -use gio::{ - prelude::{IOStreamExt, InputStreamExt}, - Cancellable, SocketConnection, -}; -use glib::{Bytes, GString, Priority}; - -pub const DEFAULT_CAPACITY: usize = 0x400; -pub const DEFAULT_MAX_SIZE: usize = 0xfffff; - -/// Body container with memory-overflow-safe, dynamically allocated [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) buffer -/// -/// **Features** -/// -/// * configurable `capacity` and `max_size` options -/// * build-in [InputStream](https://docs.gtk.org/gio/class.InputStream.html) parser -/// -/// **Notice** -/// -/// * Recommended for gemtext documents -/// * For media types, use native stream processors (e.g. [Pixbuf](https://docs.gtk.org/gdk-pixbuf/ctor.Pixbuf.new_from_stream.html) for images) -pub struct Body { - buffer: Vec, - max_size: usize, -} - -impl Body { - // Constructors - - /// Create new empty `Self` with default `capacity` and `max_size` preset - pub fn new() -> Self { - Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE)) - } - - /// Create new new `Self` with options - /// - /// Options: - /// * `capacity` initial bytes request to reduce extra memory reallocation (`DEFAULT_CAPACITY` if `None`) - /// * `max_size` max bytes to prevent memory overflow by unknown stream source (`DEFAULT_MAX_SIZE` if `None`) - pub fn new_with_options(capacity: Option, max_size: Option) -> Self { - Self { - buffer: Vec::with_capacity(match capacity { - Some(value) => value, - None => DEFAULT_CAPACITY, - }), - max_size: match max_size { - Some(value) => value, - None => DEFAULT_MAX_SIZE, - }, - } - } - - /// Simple way to create `Self` buffer from active [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - /// - /// **Options** - /// * `socket_connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from - /// * `callback` function to apply on async operations complete, return `Result)>` - /// - /// **Notes** - /// - /// * method requires entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html), - /// not just [InputStream](https://docs.gtk.org/gio/class.InputStream.html) because of async features; - /// * use this method after `Header` bytes taken from input stream connected (otherwise, take a look on high-level `Response` parser) - pub fn from_socket_connection_async( - socket_connection: SocketConnection, - callback: impl FnOnce(Result)>) + 'static, - ) { - Self::read_all_from_socket_connection_async( - Self::new(), - socket_connection, - None, - None, - None, - callback, - ); - } - - // Actions - - /// Asynchronously read all [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) - /// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to `Self.buffer` by `chunk` - /// - /// Useful to grab entire stream without risk of memory overflow (according to `Self.max_size`), - /// reduce extra memory reallocations by `capacity` option. - /// - /// **Notes** - /// - /// We are using entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) reference - /// instead of [InputStream](https://docs.gtk.org/gio/class.InputStream.html) just to keep main connection alive in the async chunks context - /// - /// **Options** - /// * `socket_connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from - /// * `cancellable` - [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) or `None::<&Cancellable>` by default - /// * `priority` - [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default - /// * `chunk` optional bytes count to read per chunk (`0x100` by default) - /// * `callback` function to apply on all async operations complete, return `Result)>` - pub fn read_all_from_socket_connection_async( - mut self, - socket_connection: SocketConnection, - cancelable: Option, - priority: Option, - chunk: Option, - callback: impl FnOnce(Result)>) + 'static, - ) { - socket_connection.input_stream().read_bytes_async( - match chunk { - Some(value) => value, - None => 0x100, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, - match cancelable.clone() { - Some(value) => Some(value), - None => None::, - } - .as_ref(), - move |result| match result { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return callback(Ok(self)); - } - - // Save chunk to buffer - if let Err(reason) = self.push(bytes) { - return callback(Err((reason, None))); - }; - - // Continue bytes read.. - self.read_all_from_socket_connection_async( - socket_connection, - cancelable, - priority, - chunk, - callback, - ); - } - Err(reason) => callback(Err((Error::InputStreamRead, Some(reason.message())))), - }, - ); - } - - /// Push [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to `Self.buffer` - /// - /// Return `Error::Overflow` on `max_size` reached - pub fn push(&mut self, bytes: Bytes) -> Result { - // Calculate new size value - let total = self.buffer.len() + bytes.len(); - - // Validate overflow - if total > self.max_size { - return Err(Error::BufferOverflow); - } - - // Success - self.buffer.push(bytes); - - Ok(total) - } - - // Setters - - /// Set new `max_size` value, `DEFAULT_MAX_SIZE` if `None` - pub fn set_max_size(&mut self, value: Option) { - self.max_size = match value { - Some(size) => size, - None => DEFAULT_MAX_SIZE, - } - } - - // Getters - - /// Get reference to `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) collected - pub fn buffer(&self) -> &Vec { - &self.buffer - } - - /// Return copy of `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) as UTF-8 vector - pub fn to_utf8(&self) -> Vec { - self.buffer - .iter() - .flat_map(|byte| byte.iter()) - .cloned() - .collect() - } - - // Intentable getters - - /// Try convert `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to GString - pub fn to_gstring(&self) -> Result { - match GString::from_utf8(self.to_utf8()) { - Ok(result) => Ok(result), - Err(_) => Err(Error::Decode), - } - } -} diff --git a/src/client/response/body/error.rs b/src/client/response/body/error.rs deleted file mode 100644 index b90b9c2..0000000 --- a/src/client/response/body/error.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub enum Error { - Decode, - InputStreamRead, - BufferOverflow, -} diff --git a/src/client/response/data.rs b/src/client/response/data.rs index ca43b82..0009fe8 100644 --- a/src/client/response/data.rs +++ b/src/client/response/data.rs @@ -1,6 +1,7 @@ //! Gemini response could have different MIME type for data. -//! Use one of these components to parse response according to content type expected. +//! 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; diff --git a/src/client/response/data/text.rs b/src/client/response/data/text.rs new file mode 100644 index 0000000..10ed150 --- /dev/null +++ b/src/client/response/data/text.rs @@ -0,0 +1,121 @@ +//! Tools for Text-based response + +pub mod error; +pub use error::Error; + +// Local dependencies +use gio::{ + prelude::{IOStreamExt, InputStreamExt}, + Cancellable, SocketConnection, +}; +use glib::{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 { + data: GString, +} + +impl Text { + // Constructors + + /// Create new `Self` with options + pub fn new(data: GString) -> Self { + Self { data } + } + + /// Create new `Self` from UTF-8 buffer + pub fn from_utf8(buffer: &[u8]) -> Result)> { + match GString::from_utf8(buffer.into()) { + Ok(data) => Ok(Self::new(data)), + Err(_) => Err((Error::Decode, None)), + } + } + + /// Asynchronously create new `Self` from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) + /// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + pub fn from_socket_connection_async( + socket_connection: SocketConnection, + priority: Option, + cancellable: Option, + on_complete: impl FnOnce(Result)>) + 'static, + ) { + read_all_from_socket_connection_async( + Vec::with_capacity(BUFFER_CAPACITY), + socket_connection, + match cancellable { + Some(value) => Some(value), + None => None::, + }, + match priority { + Some(value) => value, + None => Priority::DEFAULT, + }, + |result| match result { + Ok(buffer) => on_complete(Self::from_utf8(&buffer)), + Err(reason) => on_complete(Err(reason)), + }, + ); + } + + // Getters + + /// Get reference to `Self` data + pub fn data(&self) -> &GString { + &self.data + } +} + +// Tools + +/// Asynchronously read all bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// +/// Return UTF-8 buffer collected. +/// +/// * this function implements low-level helper for `Text::from_socket_connection_async`, also provides public API for external integrations +/// * requires `SocketConnection` instead of `InputStream` to keep connection alive (by increasing reference count in async context) @TODO +pub fn read_all_from_socket_connection_async( + mut buffer: Vec, + socket_connection: SocketConnection, + cancelable: Option, + priority: Priority, + callback: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, +) { + socket_connection.input_stream().read_bytes_async( + BUFFER_CAPACITY, + priority, + cancelable.clone().as_ref(), + 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, None))); + } + + // Save chunks to buffer + for &byte in bytes.iter() { + buffer.push(byte); + } + + // Continue bytes reading + read_all_from_socket_connection_async( + buffer, + socket_connection, + cancelable, + priority, + callback, + ); + } + Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), + }, + ); +} diff --git a/src/client/response/data/text/error.rs b/src/client/response/data/text/error.rs index 8b32c89..3007499 100644 --- a/src/client/response/data/text/error.rs +++ b/src/client/response/data/text/error.rs @@ -1,4 +1,6 @@ #[derive(Debug)] pub enum Error { - // nothing yet.. + BufferOverflow, + Decode, + InputStream, } From 97b03175094b5804c818573ce5c0a8fc3a2aa291 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 05:29:07 +0200 Subject: [PATCH 087/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a0b883d..91de888 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.7.0" +version = "0.8.0" edition = "2021" license = "MIT" readme = "README.md" From 5c0b8bf38681164ab4f8872a624ae4c3fce83ed9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 31 Oct 2024 05:32:10 +0200 Subject: [PATCH 088/392] separate constructors --- src/client/response/data/text.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/client/response/data/text.rs b/src/client/response/data/text.rs index 10ed150..0a3a2b4 100644 --- a/src/client/response/data/text.rs +++ b/src/client/response/data/text.rs @@ -22,15 +22,22 @@ pub struct Text { impl Text { // Constructors - /// Create new `Self` with options - pub fn new(data: GString) -> Self { - Self { data } + /// 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)> { match GString::from_utf8(buffer.into()) { - Ok(data) => Ok(Self::new(data)), + Ok(data) => Ok(Self::from_string(&data)), Err(_) => Err((Error::Decode, None)), } } From 8bfea4ffcfe47086e7b86d015be510b2b37b51f4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 04:32:53 +0200 Subject: [PATCH 089/392] make mime type optional --- src/client/response/meta.rs | 4 ++-- src/client/response/meta/mime.rs | 30 ++++++++++++++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index fb14f4e..926d546 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -23,7 +23,7 @@ pub const MAX_LEN: usize = 0x400; // 1024 pub struct Meta { data: Data, - mime: Mime, + mime: Option, status: Status, // @TODO // charset: Charset, @@ -129,7 +129,7 @@ impl Meta { &self.data } - pub fn mime(&self) -> &Mime { + pub fn mime(&self) -> &Option { &self.mime } } diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index 82e6c14..fd947cc 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -24,7 +24,7 @@ pub enum Mime { } // @TODO impl Mime { - pub fn from_utf8(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result, Error> { let len = buffer.len(); match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { Some(value) => match GString::from_utf8(value.into()) { @@ -53,47 +53,53 @@ impl Mime { } // @TODO extension to lowercase } - pub fn from_string(value: &str) -> Result { + pub fn from_string(value: &str) -> Result, Error> { // Text if value.contains("text/gemini") { - return Ok(Self::TextGemini); + return Ok(Some(Self::TextGemini)); } if value.contains("text/plain") { - return Ok(Self::TextPlain); + return Ok(Some(Self::TextPlain)); } // Image if value.contains("image/gif") { - return Ok(Self::ImageGif); + return Ok(Some(Self::ImageGif)); } if value.contains("image/jpeg") { - return Ok(Self::ImageJpeg); + return Ok(Some(Self::ImageJpeg)); } if value.contains("image/webp") { - return Ok(Self::ImageWebp); + return Ok(Some(Self::ImageWebp)); } if value.contains("image/png") { - return Ok(Self::ImagePng); + return Ok(Some(Self::ImagePng)); } // Audio if value.contains("audio/flac") { - return Ok(Self::AudioFlac); + return Ok(Some(Self::AudioFlac)); } if value.contains("audio/mpeg") { - return Ok(Self::AudioMpeg); + return Ok(Some(Self::AudioMpeg)); } if value.contains("audio/ogg") { - return Ok(Self::AudioOgg); + return Ok(Some(Self::AudioOgg)); } - Err(Error::Undefined) + // Some type exist, but not defined yet + if value.contains("/") { + return Err(Error::Undefined); + } + + // Done + Ok(None) // may be empty (for some status codes) } pub fn from_uri(uri: &Uri) -> Result { From fc48a08be960ce82f44c1be2b91bfb440f18408b Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:02:56 +0200 Subject: [PATCH 090/392] add comments --- src/client/response/meta/mime.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index fd947cc..7448d81 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -1,11 +1,16 @@ +//! MIME type parser for different data types: +//! +//! * UTF-8 buffer with entire response or just with meta slice (that include **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; -pub const MAX_LEN: usize = 0x400; // 1024 - /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters #[derive(Debug)] pub enum Mime { @@ -24,8 +29,18 @@ pub enum Mime { } // @TODO impl Mime { + /// Create new `Self` from UTF-8 buffer + /// + /// * 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, Error> { + // Define max buffer length for this parser + 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()), @@ -35,6 +50,7 @@ impl Mime { } } + /// Create new `Self` from `std::Path` pub fn from_path(path: &Path) -> Result { match path.extension().and_then(|extension| extension.to_str()) { // Text @@ -53,6 +69,10 @@ impl Mime { } // @TODO extension to lowercase } + /// Create new `Self` from string 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 pub fn from_string(value: &str) -> Result, Error> { // Text if value.contains("text/gemini") { @@ -102,6 +122,7 @@ impl Mime { Ok(None) // may be empty (for some status codes) } + /// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html) pub fn from_uri(uri: &Uri) -> Result { Self::from_path(Path::new(&uri.to_string())) } From 7d837c552b6c19074af85187d758f3e0c7616760 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:05:41 +0200 Subject: [PATCH 091/392] update comments --- src/client/response/meta/mime.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index 7448d81..979871e 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -50,7 +50,7 @@ impl Mime { } } - /// Create new `Self` from `std::Path` + /// Create new `Self` from `std::Path` that includes file **extension** pub fn from_path(path: &Path) -> Result { match path.extension().and_then(|extension| extension.to_str()) { // Text @@ -123,6 +123,7 @@ impl Mime { } /// 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::from_path(Path::new(&uri.to_string())) } From a0827e04250ed20818e5f8c79dc2e52bd8af40c1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:09:35 +0200 Subject: [PATCH 092/392] update comments --- src/client/response/meta.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 926d546..9b6c031 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -34,10 +34,12 @@ impl Meta { // Constructors /// Create new `Self` from UTF-8 buffer + /// * supports entire response or just meta slice pub fn from_utf8(buffer: &[u8]) -> Result)> { + // Calculate buffer length once let len = buffer.len(); - // Can parse from entire response or just meta buffer given + // Parse meta bytes only match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { Some(slice) => { // Parse data From 10205f3147513d5a614a7ea37ee5f73893e936e2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:21:47 +0200 Subject: [PATCH 093/392] make data optional on empty --- src/client/response/meta.rs | 6 +++--- src/client/response/meta/data.rs | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 9b6c031..0ffcaa6 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -22,9 +22,9 @@ use glib::Priority; pub const MAX_LEN: usize = 0x400; // 1024 pub struct Meta { - data: Data, - mime: Option, status: Status, + data: Option, + mime: Option, // @TODO // charset: Charset, // language: Language, @@ -127,7 +127,7 @@ impl Meta { &self.status } - pub fn data(&self) -> &Data { + pub fn data(&self) -> &Option { &self.data } diff --git a/src/client/response/meta/data.rs b/src/client/response/meta/data.rs index d903f08..737b21c 100644 --- a/src/client/response/meta/data.rs +++ b/src/client/response/meta/data.rs @@ -13,12 +13,15 @@ pub const MAX_LEN: usize = 0x400; // 1024 /// * placeholder for 10, 11 status /// * URL for 30, 31 status pub struct Data { - value: Option, + value: GString, } impl Data { - /// Parse Meta from UTF-8 - pub fn from_utf8(buffer: &[u8]) -> Result { + /// Parse meta data from UTF-8 buffer + /// + /// * 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, Error> { // Init bytes buffer let mut bytes: Vec = Vec::with_capacity(MAX_LEN); @@ -40,11 +43,9 @@ impl Data { // Assumes the bytes are valid UTF-8 match GString::from_utf8(bytes) { - Ok(value) => Ok(Self { - value: match value.is_empty() { - false => Some(value), - true => None, - }, + Ok(value) => Ok(match value.is_empty() { + false => Some(Self { value }), + true => None, }), Err(_) => Err(Error::Decode), } @@ -53,7 +54,7 @@ impl Data { } } - pub fn value(&self) -> &Option { + pub fn value(&self) -> &GString { &self.value } } From 3f6688b82429887414feca24afc9bd0281692ff9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:22:14 +0200 Subject: [PATCH 094/392] update comments --- src/client/response/meta.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 0ffcaa6..036b53a 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -26,8 +26,8 @@ pub struct Meta { data: Option, mime: Option, // @TODO - // charset: Charset, - // language: Language, + // charset: Option, + // language: Option, } impl Meta { From 748ccabebb9f7e0e279d49567ea23395ae4183f1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:28:36 +0200 Subject: [PATCH 095/392] update comments --- src/client/response/meta/data.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/client/response/meta/data.rs b/src/client/response/meta/data.rs index 737b21c..f0fd1cd 100644 --- a/src/client/response/meta/data.rs +++ b/src/client/response/meta/data.rs @@ -1,3 +1,6 @@ +//! 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; @@ -5,19 +8,18 @@ use glib::GString; pub const MAX_LEN: usize = 0x400; // 1024 -/// Meta data holder for response +/// Meta **data** holder /// -/// Could be created from entire response buffer or just header slice -/// -/// Use as: -/// * placeholder for 10, 11 status -/// * URL for 30, 31 status +/// For example, `value` could contain: +/// * placeholder text for 10, 11 status +/// * URL string for 30, 31 status pub struct Data { value: GString, } impl Data { - /// Parse meta data from UTF-8 buffer + /// 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 From 1b1ab6bea046c471707f33b4041c4ffdb3d66093 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:29:45 +0200 Subject: [PATCH 096/392] update comment --- src/client/response/meta/mime.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index 979871e..b256626 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -34,7 +34,7 @@ impl Mime { /// * 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, Error> { - // Define max buffer length for this parser + // Define max buffer length for this method const MAX_LEN: usize = 0x400; // 1024 // Calculate buffer length once From 5e315c02592c86321cc4b1b86066304dd5edf8c0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:30:40 +0200 Subject: [PATCH 097/392] make MAX_LEN local --- src/client/response/meta/data.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/client/response/meta/data.rs b/src/client/response/meta/data.rs index f0fd1cd..b83054e 100644 --- a/src/client/response/meta/data.rs +++ b/src/client/response/meta/data.rs @@ -6,8 +6,6 @@ pub use error::Error; use glib::GString; -pub const MAX_LEN: usize = 0x400; // 1024 - /// Meta **data** holder /// /// For example, `value` could contain: @@ -18,12 +16,17 @@ pub struct Data { } 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, Error> { + // Define max buffer length for this method + const MAX_LEN: usize = 0x400; // 1024 + // Init bytes buffer let mut bytes: Vec = Vec::with_capacity(MAX_LEN); @@ -56,6 +59,8 @@ impl Data { } } + // Getters + pub fn value(&self) -> &GString { &self.value } From 0d0ce49d981e7b629e8211e044195549b31a9d88 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:33:30 +0200 Subject: [PATCH 098/392] update description --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 91de888..7ee8209 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.8.0" edition = "2021" license = "MIT" readme = "README.md" -description = "Glib-oriented client for Gemini protocol" +description = "Glib/Gio-oriented network API for Gemini protocol" keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"] categories = ["development-tools", "network-programming", "parsing"] repository = "https://github.com/YGGverse/ggemini" From 85ea1ef0b2eccdcdaac9c5891a78120beec99d65 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:34:37 +0200 Subject: [PATCH 099/392] update readme --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index e2cf971..4bd737e 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,6 @@ cargo add ggemini * [Documentation](https://docs.rs/ggemini/latest/ggemini/) -_todo_ - -### `client` -### `gio` - ## See also * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file From b6134c92d528c833b0c2cf6efd7ef1f27a346b2c Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 05:37:00 +0200 Subject: [PATCH 100/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7ee8209..050179e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.8.0" +version = "0.9.0" edition = "2021" license = "MIT" readme = "README.md" From ac17a48144f92ba8e1ed814e01cfd18d5dcb7718 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 17:51:52 +0200 Subject: [PATCH 101/392] add svg image format --- src/client/response/meta/mime.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index b256626..8665c7b 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -21,6 +21,7 @@ pub enum Mime { ImageGif, ImageJpeg, ImagePng, + ImageSvg, ImageWebp, // Audio AudioFlac, @@ -56,11 +57,14 @@ impl Mime { // 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), @@ -92,14 +96,18 @@ impl Mime { return Ok(Some(Self::ImageJpeg)); } - if value.contains("image/webp") { - return Ok(Some(Self::ImageWebp)); - } - 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)); From d34d24588fb7ecb476b49962b33a0ff7ee94ac16 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 18:54:19 +0200 Subject: [PATCH 102/392] hold code in status enum --- src/client/response/meta/status.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client/response/meta/status.rs b/src/client/response/meta/status.rs index 2875d31..3cd1244 100644 --- a/src/client/response/meta/status.rs +++ b/src/client/response/meta/status.rs @@ -6,14 +6,14 @@ use glib::GString; /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes #[derive(Debug)] pub enum Status { - // 10 | 11 - Input, - SensitiveInput, - // 20 - Success, - // 30 | 31 - Redirect, - PermanentRedirect, + // Input + Input = 10, + SensitiveInput = 11, + // Success + Success = 20, + // Redirect + Redirect = 30, + PermanentRedirect = 31, } // @TODO impl Status { From 9dd1bcc9dd746639dec75ea781b39856021285b3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:07:26 +0200 Subject: [PATCH 103/392] implement all status codes support --- src/client/response/meta/status.rs | 45 ++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/client/response/meta/status.rs b/src/client/response/meta/status.rs index 3cd1244..45f386b 100644 --- a/src/client/response/meta/status.rs +++ b/src/client/response/meta/status.rs @@ -1,20 +1,42 @@ +//! Parser and holder API for [Status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) + pub mod error; pub use error::Error; use glib::GString; -/// 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, -} // @TODO + + // 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 { pub fn from_utf8(buffer: &[u8]) -> Result { @@ -29,11 +51,30 @@ impl Status { pub fn from_string(code: &str) -> Result { 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), } } From c4fa868345da1cfcfeac3729a3756d1854560a0e Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:12:19 +0200 Subject: [PATCH 104/392] add documentation comments --- src/client/response/meta/status.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/response/meta/status.rs b/src/client/response/meta/status.rs index 45f386b..1d0c719 100644 --- a/src/client/response/meta/status.rs +++ b/src/client/response/meta/status.rs @@ -1,37 +1,34 @@ -//! Parser and holder API for [Status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) +//! 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, @@ -39,6 +36,9 @@ pub enum Status { } 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 { match buffer.get(0..2) { Some(value) => match GString::from_utf8(value.to_vec()) { @@ -49,6 +49,7 @@ impl Status { } } + /// Create new `Self` from string that includes **header** pub fn from_string(code: &str) -> Result { match code { // Input From cac4168645a4e4582629646b99cd8b98996e7980 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:19:33 +0200 Subject: [PATCH 105/392] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4bd737e..51e48be 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol, -it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda). +Useful for GTK-based applications, require [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) only ## Install From cd9b132952ffecb32cd252304ae6e2810c5db6f4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:20:44 +0200 Subject: [PATCH 106/392] update readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 51e48be..5c0868a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda). -Useful for GTK-based applications, require [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) only +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda) + +This library could be useful for other GTK-based applications, require [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) only ## Install From cb56e614ae6dddbc2195e97212123775034a7744 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:23:14 +0200 Subject: [PATCH 107/392] update readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c0868a..a038620 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda) - -This library could be useful for other GTK-based applications, require [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) only +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications, that require [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates. ## Install From 6e46ddb676e2432d952d224f410d2df459e42c67 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:26:37 +0200 Subject: [PATCH 108/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a038620..ad2cc77 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications, that require [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) requirements. ## Install From 6435a88cb6ebce065e4407cdaaa5a5166b0aafbc Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:27:46 +0200 Subject: [PATCH 109/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad2cc77..562d13f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) requirements. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependencies. ## Install From 5aaea573046b2aca1d6bf2c431ee9557d3bfd3d1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:28:33 +0200 Subject: [PATCH 110/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 562d13f..5bc8878 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependencies. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications for Gemini protocol with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependencies. ## Install From fab352e0b5c5e5849dfda727b5b465a806695694 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:31:15 +0200 Subject: [PATCH 111/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5bc8878..562d13f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications for Gemini protocol with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependencies. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependencies. ## Install From 03b2c361539dcb8416d8fb731f425d53e5a36186 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 1 Nov 2024 19:54:25 +0200 Subject: [PATCH 112/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 562d13f..5eb2487 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependencies. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependency. ## Install From 7215258a71795bbef8636bef3f2d635543547e8b Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 2 Nov 2024 22:45:06 +0200 Subject: [PATCH 113/392] disable unstable feature --- src/client/response/meta/mime.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index 8665c7b..e68b014 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -122,9 +122,10 @@ impl Mime { } // Some type exist, but not defined yet + /* @TODO unstable if value.contains("/") { return Err(Error::Undefined); - } + } */ // Done Ok(None) // may be empty (for some status codes) From c1c06667c9a5efb0256ebdef4502cdd27b3cd02f Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 3 Nov 2024 00:11:05 +0200 Subject: [PATCH 114/392] fix undefined mime type detection --- src/client/response/meta/mime.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index e68b014..6dabf42 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -1,6 +1,6 @@ //! MIME type parser for different data types: //! -//! * UTF-8 buffer with entire response or just with meta slice (that include **header**) +//! * 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**) @@ -30,10 +30,12 @@ pub enum Mime { } // @TODO impl Mime { - /// Create new `Self` from UTF-8 buffer + /// 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) + /// * 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, Error> { // Define max buffer length for this method const MAX_LEN: usize = 0x400; // 1024 @@ -75,8 +77,10 @@ impl Mime { /// Create new `Self` from string 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 + /// **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, Error> { // Text if value.contains("text/gemini") { @@ -121,14 +125,13 @@ impl Mime { return Ok(Some(Self::AudioOgg)); } - // Some type exist, but not defined yet - /* @TODO unstable - if value.contains("/") { + // 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 (for some status codes) + Ok(None) // may be empty (status code ^2*) } /// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html) From d6dc4d68704f9717d829fdae3b34c9741b1ec862 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 15 Nov 2024 21:46:52 +0200 Subject: [PATCH 115/392] replace SocketConnection with IOStream implementation (for future client certificate support) --- Cargo.toml | 2 +- src/client/response/data/text.rs | 38 +++++++---------- src/client/response/meta.rs | 42 +++++++------------ src/gio/memory_input_stream.rs | 71 ++++++++------------------------ 4 files changed, 49 insertions(+), 104 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 050179e..af4656e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.9.0" +version = "0.10.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/client/response/data/text.rs b/src/client/response/data/text.rs index 0a3a2b4..dfce6a1 100644 --- a/src/client/response/data/text.rs +++ b/src/client/response/data/text.rs @@ -6,9 +6,9 @@ pub use error::Error; // Local dependencies use gio::{ prelude::{IOStreamExt, InputStreamExt}, - Cancellable, SocketConnection, + Cancellable, IOStream, }; -use glib::{GString, Priority}; +use glib::{object::IsA, GString, Priority}; // Default limits pub const BUFFER_CAPACITY: usize = 0x400; // 1024 @@ -42,17 +42,16 @@ impl Text { } } - /// Asynchronously create new `Self` from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) - /// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - pub fn from_socket_connection_async( - socket_connection: SocketConnection, + /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + pub fn from_stream_async( + stream: impl IsA, priority: Option, cancellable: Option, on_complete: impl FnOnce(Result)>) + 'static, ) { - read_all_from_socket_connection_async( + read_all_from_stream_async( Vec::with_capacity(BUFFER_CAPACITY), - socket_connection, + stream, match cancellable { Some(value) => Some(value), None => None::, @@ -78,21 +77,18 @@ impl Text { // Tools -/// Asynchronously read all bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) -/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// Asynchronously read all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// -/// Return UTF-8 buffer collected. -/// -/// * this function implements low-level helper for `Text::from_socket_connection_async`, also provides public API for external integrations -/// * requires `SocketConnection` instead of `InputStream` to keep connection alive (by increasing reference count in async context) @TODO -pub fn read_all_from_socket_connection_async( +/// 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, - socket_connection: SocketConnection, + stream: impl IsA, cancelable: Option, priority: Priority, callback: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, ) { - socket_connection.input_stream().read_bytes_async( + stream.input_stream().read_bytes_async( BUFFER_CAPACITY, priority, cancelable.clone().as_ref(), @@ -114,13 +110,7 @@ pub fn read_all_from_socket_connection_async( } // Continue bytes reading - read_all_from_socket_connection_async( - buffer, - socket_connection, - cancelable, - priority, - callback, - ); + read_all_from_stream_async(buffer, stream, cancelable, priority, callback); } Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), }, diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 036b53a..7e8ba71 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -15,9 +15,9 @@ pub use status::Status; use gio::{ prelude::{IOStreamExt, InputStreamExtManual}, - Cancellable, SocketConnection, + Cancellable, IOStream, }; -use glib::Priority; +use glib::{object::IsA, Priority}; pub const MAX_LEN: usize = 0x400; // 1024 @@ -95,17 +95,16 @@ impl Meta { } } - /// Asynchronously create new `Self` from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) - /// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - pub fn from_socket_connection_async( - socket_connection: SocketConnection, + /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + pub fn from_stream_async( + stream: impl IsA, priority: Option, cancellable: Option, on_complete: impl FnOnce(Result)>) + 'static, ) { - read_from_socket_connection_async( + read_from_stream_async( Vec::with_capacity(MAX_LEN), - socket_connection, + stream, match cancellable { Some(value) => Some(value), None => None::, @@ -138,21 +137,18 @@ impl Meta { // Tools -/// Asynchronously read all meta bytes from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) -/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// Asynchronously read all meta bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// -/// Return UTF-8 buffer collected. -/// -/// * this function implements low-level helper for `Meta::from_socket_connection_async`, also provides public API for external integrations -/// * requires `SocketConnection` instead of `InputStream` to keep connection alive (by increasing reference count in async context) @TODO -pub fn read_from_socket_connection_async( +/// Return UTF-8 buffer collected +/// * require `IOStream` reference to keep `Connection` active in async thread +pub fn read_from_stream_async( mut buffer: Vec, - connection: SocketConnection, + stream: impl IsA, cancellable: Option, priority: Priority, on_complete: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, ) { - connection.input_stream().read_async( + stream.input_stream().read_async( vec![0], priority, cancellable.clone().as_ref(), @@ -165,9 +161,9 @@ pub fn read_from_socket_connection_async( // Read next byte without record if bytes.contains(&b'\r') { - return read_from_socket_connection_async( + return read_from_stream_async( buffer, - connection, + stream, cancellable, priority, on_complete, @@ -183,13 +179,7 @@ pub fn read_from_socket_connection_async( buffer.append(&mut bytes); // Continue - read_from_socket_connection_async( - buffer, - connection, - cancellable, - priority, - on_complete, - ); + read_from_stream_async(buffer, stream, cancellable, priority, on_complete); } Err((_, reason)) => on_complete(Err((Error::InputStream, Some(reason.message())))), }, diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 1c8d193..09f9339 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -3,33 +3,18 @@ pub use error::Error; use gio::{ prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, - Cancellable, MemoryInputStream, SocketConnection, + Cancellable, IOStream, MemoryInputStream, }; -use glib::{Bytes, Priority}; +use glib::{object::IsA, Bytes, Priority}; /// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) -/// from [InputStream](https://docs.gtk.org/gio/class.InputStream.html) -/// for given [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// -/// Useful to create dynamically allocated, memory-safe buffer -/// from remote connections, where final size of target data could not be known by Gemini protocol restrictions. -/// Also, could be useful for [Pixbuf](https://docs.gtk.org/gdk-pixbuf/class.Pixbuf.html) or -/// loading widgets like [Spinner](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Spinner.html) -/// to display bytes on async data loading. -/// -/// * this function takes entire `SocketConnection` reference (not `MemoryInputStream`) just to keep connection alive in the async context -/// -/// **Implementation** -/// -/// Implements low-level `read_all_from_socket_connection_async` function: -/// * recursively read all bytes from `InputStream` for `SocketConnection` according to `bytes_in_chunk` argument -/// * calculates total bytes length on every chunk iteration, validate sum with `bytes_total_limit` argument -/// * stop reading `InputStream` with `Result` on zero bytes in chunk received -/// * applies callback functions: -/// * `on_chunk` - return reference to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) and `bytes_total` collected for every chunk in reading loop -/// * `on_complete` - return `MemoryInputStream` on success or `Error` on failure as `Result` -pub fn from_socket_connection_async( - socket_connection: SocketConnection, +/// **Useful for** +/// * safe read (of memory overflow) to dynamically allocated buffer, where final size of target data unknown +/// * calculate bytes processed on chunk load +pub fn from_stream_async( + base_io_stream: impl IsA, cancelable: Option, priority: Priority, bytes_in_chunk: usize, @@ -37,9 +22,9 @@ pub fn from_socket_connection_async( on_chunk: impl Fn((Bytes, usize)) + 'static, on_complete: impl FnOnce(Result)>) + 'static, ) { - read_all_from_socket_connection_async( + read_all_from_stream_async( MemoryInputStream::new(), - socket_connection, + base_io_stream, cancelable, priority, bytes_in_chunk, @@ -50,32 +35,12 @@ pub fn from_socket_connection_async( ); } -/// Low-level helper for `from_socket_connection_async` function, -/// also provides public API for external usage. -/// -/// Asynchronously read [InputStream](https://docs.gtk.org/gio/class.InputStream.html) -/// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) -/// to given [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html). -/// -/// Useful to create dynamically allocated, memory-safe buffer -/// from remote connections, where final size of target data could not be known by Gemini protocol restrictions. -/// Also, could be useful for [Pixbuf](https://docs.gtk.org/gdk-pixbuf/class.Pixbuf.html) or -/// loading widgets like [Spinner](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Spinner.html) -/// to display bytes on async data loading. -/// -/// * this function takes entire `SocketConnection` reference (not `MemoryInputStream`) just to keep connection alive in the async context -/// -/// **Implementation** -/// -/// * recursively read all bytes from `InputStream` for `SocketConnection` according to `bytes_in_chunk` argument -/// * calculates total bytes length on every chunk iteration, validate sum with `bytes_total_limit` argument -/// * stop reading `InputStream` with `Result` on zero bytes in chunk received, otherwise continue next chunk request in loop -/// * applies callback functions: -/// * `on_chunk` - return reference to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) and `bytes_total` collected for every chunk in reading loop -/// * `on_complete` - return `MemoryInputStream` on success or `Error` on failure as `Result` -pub fn read_all_from_socket_connection_async( +/// Asynchronously read entire [InputStream](https://docs.gtk.org/gio/class.InputStream.html) +/// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// * require `IOStream` reference to keep `Connection` active in async thread +pub fn read_all_from_stream_async( memory_input_stream: MemoryInputStream, - socket_connection: SocketConnection, + base_io_stream: impl IsA, cancelable: Option, priority: Priority, bytes_in_chunk: usize, @@ -84,7 +49,7 @@ pub fn read_all_from_socket_connection_async( on_chunk: impl Fn((Bytes, usize)) + 'static, on_complete: impl FnOnce(Result)>) + 'static, ) { - socket_connection.input_stream().read_bytes_async( + base_io_stream.input_stream().read_bytes_async( bytes_in_chunk, priority, cancelable.clone().as_ref(), @@ -110,9 +75,9 @@ pub fn read_all_from_socket_connection_async( memory_input_stream.add_bytes(&bytes); // Continue - read_all_from_socket_connection_async( + read_all_from_stream_async( memory_input_stream, - socket_connection, + base_io_stream, cancelable, priority, bytes_in_chunk, From 1d4dc5038051bc3eccd0827c4b49c893adce38c5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 15 Nov 2024 22:01:47 +0200 Subject: [PATCH 116/392] apply some clippy corrections --- src/client/response/meta.rs | 6 +++--- src/client/response/meta/data.rs | 2 +- src/client/response/meta/mime.rs | 4 ++-- src/gio/memory_input_stream.rs | 27 ++++++++++++--------------- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 7e8ba71..82b5f3e 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -43,7 +43,7 @@ impl Meta { match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { Some(slice) => { // Parse data - let data = Data::from_utf8(&slice); + let data = Data::from_utf8(slice); if let Err(reason) = data { return Err(( @@ -57,7 +57,7 @@ impl Meta { // MIME - let mime = Mime::from_utf8(&slice); + let mime = Mime::from_utf8(slice); if let Err(reason) = mime { return Err(( @@ -72,7 +72,7 @@ impl Meta { // Status - let status = Status::from_utf8(&slice); + let status = Status::from_utf8(slice); if let Err(reason) = status { return Err(( diff --git a/src/client/response/meta/data.rs b/src/client/response/meta/data.rs index b83054e..710d4f6 100644 --- a/src/client/response/meta/data.rs +++ b/src/client/response/meta/data.rs @@ -22,7 +22,7 @@ impl Data { /// 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 + /// that does not expect any data in header pub fn from_utf8(buffer: &[u8]) -> Result, Error> { // Define max buffer length for this method const MAX_LEN: usize = 0x400; // 1024 diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index 6dabf42..ec8849d 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -33,9 +33,9 @@ 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 + /// 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) + /// it means that given buffer should contain some **header** (not filepath or any other type of strings) pub fn from_utf8(buffer: &[u8]) -> Result, Error> { // Define max buffer length for this method const MAX_LEN: usize = 0x400; // 1024 diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 09f9339..ce7bb47 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -27,11 +27,8 @@ pub fn from_stream_async( base_io_stream, cancelable, priority, - bytes_in_chunk, - bytes_total_limit, - 0, // initial `bytes_total` value - on_chunk, - on_complete, + (bytes_in_chunk, bytes_total_limit, 0), + (on_chunk, on_complete), ); } @@ -43,12 +40,15 @@ pub fn read_all_from_stream_async( base_io_stream: impl IsA, cancelable: Option, priority: Priority, - bytes_in_chunk: usize, - bytes_total_limit: usize, - bytes_total: usize, - on_chunk: impl Fn((Bytes, usize)) + 'static, - on_complete: impl FnOnce(Result)>) + 'static, + bytes: (usize, usize, usize), + callback: ( + impl Fn((Bytes, usize)) + 'static, + impl FnOnce(Result)>) + 'static, + ), ) { + let (on_chunk, on_complete) = callback; + let (bytes_in_chunk, bytes_total_limit, bytes_total) = bytes; + base_io_stream.input_stream().read_bytes_async( bytes_in_chunk, priority, @@ -80,11 +80,8 @@ pub fn read_all_from_stream_async( base_io_stream, cancelable, priority, - bytes_in_chunk, - bytes_total_limit, - bytes_total, - on_chunk, - on_complete, + (bytes_in_chunk, bytes_total_limit, bytes_total), + (on_chunk, on_complete), ); } Err(reason) => { From af7aaa63e4ae8b3db4eb4e860ebfcbf58d11d1ce Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 15 Nov 2024 22:20:02 +0200 Subject: [PATCH 117/392] add Default implementation for Text --- src/client/response/data/text.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/response/data/text.rs b/src/client/response/data/text.rs index dfce6a1..f2c814b 100644 --- a/src/client/response/data/text.rs +++ b/src/client/response/data/text.rs @@ -19,6 +19,12 @@ pub struct Text { data: GString, } +impl Default for Text { + fn default() -> Self { + Self::new() + } +} + impl Text { // Constructors From 32a879b756d88b11e347ccadee209759b75179e4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 16 Nov 2024 09:18:25 +0200 Subject: [PATCH 118/392] require gio 2.70+ --- Cargo.toml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index af4656e..6142630 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" version = "0.20.4" +features = ["v2_70"] [dependencies.glib] package = "glib" diff --git a/README.md b/README.md index 5eb2487..43d67af 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependency. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) (`2.70+`) and [gio](https://crates.io/crates/gio) (`2.66+`) dependency. ## Install From 67d486cc4d2752e6de2643bbbbb8ccff699eb798 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 16 Nov 2024 09:34:37 +0200 Subject: [PATCH 119/392] remove gio 2.70+ requirement --- Cargo.toml | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6142630..512c152 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,8 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" version = "0.20.4" -features = ["v2_70"] +# currently not required +# features = ["v2_70"] [dependencies.glib] package = "glib" diff --git a/README.md b/README.md index 43d67af..5eb2487 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) (`2.70+`) and [gio](https://crates.io/crates/gio) (`2.66+`) dependency. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependency. ## Install From 3a9e84a3d93dab0161c4b1d86ac8c4aae404d0cd Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 05:50:09 +0200 Subject: [PATCH 120/392] draft new api version --- Cargo.toml | 2 +- src/client.rs | 128 ++++++++++++++++++++++- src/client/connection.rs | 82 +++++++++++++++ src/client/connection/certificate.rs | 52 +++++++++ src/client/connection/error.rs | 16 +++ src/client/error.rs | 36 +++++++ src/client/response.rs | 29 +++++ src/client/response/data/text.rs | 21 ++-- src/client/response/data/text/error.rs | 22 +++- src/client/response/error.rs | 20 ++++ src/client/response/meta.rs | 58 +++------- src/client/response/meta/data.rs | 10 +- src/client/response/meta/data/error.rs | 17 ++- src/client/response/meta/error.rs | 38 +++++-- src/client/response/meta/mime.rs | 2 +- src/client/response/meta/mime/error.rs | 20 +++- src/client/response/meta/status.rs | 2 +- src/client/response/meta/status/error.rs | 20 +++- src/lib.rs | 2 + 19 files changed, 490 insertions(+), 87 deletions(-) create mode 100644 src/client/connection.rs create mode 100644 src/client/connection/certificate.rs create mode 100644 src/client/connection/error.rs create mode 100644 src/client/error.rs create mode 100644 src/client/response/error.rs diff --git a/Cargo.toml b/Cargo.toml index 512c152..e27248d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.10.0" +version = "0.11.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/client.rs b/src/client.rs index 7335b61..ed70099 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,128 @@ -//! Client API to interact Server using -//! [Gemini protocol](https://geminiprotocol.net/docs/protocol-specification.gmi) +//! High-level client API to interact with Gemini Socket Server: +//! * https://geminiprotocol.net/docs/protocol-specification.gmi +pub mod connection; +pub mod error; pub mod response; + +pub use connection::Connection; +pub use error::Error; +pub use response::Response; + +use gio::{ + prelude::{IOStreamExt, OutputStreamExt, SocketClientExt}, + Cancellable, NetworkAddress, SocketClient, SocketProtocol, TlsCertificate, +}; +use glib::{Bytes, Priority, Uri}; + +pub const DEFAULT_PORT: u16 = 1965; +pub const DEFAULT_TIMEOUT: u32 = 10; + +pub struct Client { + pub socket: SocketClient, +} + +impl Client { + // Constructors + + /// Create new `Self` + pub fn new() -> Self { + let socket = SocketClient::new(); + + socket.set_protocol(SocketProtocol::Tcp); + socket.set_timeout(DEFAULT_TIMEOUT); + + Self { socket } + } + + // Actions + + /// Make async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), + /// callback with `Result`on success or `Error` on failure. + /// * creates new [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + /// * session management by Glib TLS Backend + pub fn request_async( + &self, + uri: Uri, + priority: Option, + cancellable: Option, + certificate: Option, + callback: impl Fn(Result) + 'static, + ) { + match network_address_for(&uri) { + Ok(network_address) => { + self.socket.connect_async( + &network_address.clone(), + match cancellable { + Some(ref cancellable) => Some(cancellable.clone()), + None => None::, + } + .as_ref(), + move |result| match result { + Ok(connection) => { + match Connection::from(network_address, connection, certificate) { + Ok(result) => request_async( + result, + uri.to_string(), + match priority { + Some(priority) => priority, + None => Priority::DEFAULT, + }, + cancellable.unwrap(), // @TODO + move |result| callback(result), + ), + Err(reason) => callback(Err(Error::Connection(reason))), + } + } + Err(reason) => callback(Err(Error::Connect(reason))), + }, + ); + } + Err(reason) => callback(Err(reason)), + }; + } +} + +// Private helpers + +/// [SocketConnectable](https://docs.gtk.org/gio/iface.SocketConnectable.html) / +/// [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) +fn network_address_for(uri: &Uri) -> Result { + Ok(NetworkAddress::new( + &match uri.host() { + Some(host) => host, + None => return Err(Error::Connectable(uri.to_string())), + }, + if uri.port().is_positive() { + uri.port() as u16 + } else { + DEFAULT_PORT + }, + )) +} + +fn request_async( + connection: Connection, + query: String, + priority: Priority, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, +) { + connection.stream().output_stream().write_bytes_async( + &Bytes::from(format!("{query}\r\n").as_bytes()), + priority, + Some(&cancellable.clone()), + move |result| match result { + Ok(_) => Response::from_request_async( + connection, + Some(priority), + Some(cancellable), + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(reason) => callback(Err(Error::Response(reason))), + }, + ), + Err(reason) => callback(Err(Error::Write(reason))), + }, + ); +} diff --git a/src/client/connection.rs b/src/client/connection.rs new file mode 100644 index 0000000..d11f006 --- /dev/null +++ b/src/client/connection.rs @@ -0,0 +1,82 @@ +pub mod certificate; +pub mod error; + +pub use certificate::Certificate; +pub use error::Error; + +use gio::{ + prelude::{IOStreamExt, TlsConnectionExt}, + IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, +}; +use glib::object::{Cast, IsA}; + +pub struct Connection { + pub socket_connection: SocketConnection, + pub tls_client_connection: Option, +} + +impl Connection { + // Constructors + + /// Create new `Self` + pub fn from( + network_address: NetworkAddress, // @TODO struct cert as sni + socket_connection: SocketConnection, + certificate: Option, + ) -> Result { + if socket_connection.is_closed() { + return Err(Error::Closed); + } + + Ok(Self { + socket_connection: socket_connection.clone(), + tls_client_connection: match certificate { + Some(certificate) => match auth(network_address, socket_connection, certificate) { + Ok(tls_client_connection) => Some(tls_client_connection), + Err(reason) => return Err(reason), + }, + None => None, + }, + }) + } + + // Getters + + pub fn stream(&self) -> impl IsA { + match self.tls_client_connection.clone() { + Some(tls_client_connection) => tls_client_connection.upcast::(), + None => self.socket_connection.clone().upcast::(), + } + } +} + +// Tools + +pub fn auth( + server_identity: NetworkAddress, // @TODO impl IsA ? + socket_connection: SocketConnection, + certificate: TlsCertificate, +) -> Result { + if socket_connection.is_closed() { + return Err(Error::Closed); + } + + // https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls + match TlsClientConnection::new(&socket_connection, Some(&server_identity)) { + Ok(tls_client_connection) => { + // https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates + tls_client_connection.set_certificate(&certificate); + + // @TODO handle exceptions + // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections + tls_client_connection.set_require_close_notify(true); + + // @TODO host validation + // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation + tls_client_connection.connect_accept_certificate(move |_, _, _| true); + + Ok(tls_client_connection) + } + Err(reason) => Err(Error::Tls(reason)), + } +} diff --git a/src/client/connection/certificate.rs b/src/client/connection/certificate.rs new file mode 100644 index 0000000..f33e2f4 --- /dev/null +++ b/src/client/connection/certificate.rs @@ -0,0 +1,52 @@ +pub mod error; +pub mod scope; + +pub use error::Error; +pub use scope::Scope; + +use gio::{prelude::TlsCertificateExt, TlsCertificate}; +use glib::DateTime; + +pub struct Certificate { + tls_certificate: TlsCertificate, +} + +impl Certificate { + // Constructors + + /// Create new `Self` + pub fn from_pem(pem: &str) -> Result { + Ok(Self { + tls_certificate: match TlsCertificate::from_pem(&pem) { + Ok(tls_certificate) => { + // Validate expiration time + match DateTime::now_local() { + Ok(now_local) => { + match tls_certificate.not_valid_after() { + Some(not_valid_after) => { + if now_local > not_valid_after { + return Err(Error::Expired(not_valid_after)); + } + } + None => return Err(Error::ValidAfter), + } + match tls_certificate.not_valid_before() { + Some(not_valid_before) => { + if now_local < not_valid_before { + return Err(Error::Inactive(not_valid_before)); + } + } + None => return Err(Error::ValidBefore), + } + } + Err(_) => return Err(Error::DateTime), + } + + // Success + tls_certificate + } + Err(reason) => return Err(Error::Decode(reason)), + }, + }) + } +} diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs new file mode 100644 index 0000000..aec2687 --- /dev/null +++ b/src/client/connection/error.rs @@ -0,0 +1,16 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Closed, + Tls(glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Closed => write!(f, "Socket connection closed"), + Self::Tls(reason) => write!(f, "Could not create TLS connection: {reason}"), + } + } +} diff --git a/src/client/error.rs b/src/client/error.rs new file mode 100644 index 0000000..a9eb0ac --- /dev/null +++ b/src/client/error.rs @@ -0,0 +1,36 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Connectable(String), + Connection(super::connection::Error), + Connect(glib::Error), + Request(glib::Error), + Response(super::response::Error), + Write(glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Connectable(uri) => { + write!(f, "Could not create connectable address for {uri}") + } + Self::Connection(reason) => { + write!(f, "Connection error: {reason}") + } + Self::Connect(reason) => { + write!(f, "Connect error: {reason}") + } + Self::Request(reason) => { + write!(f, "Request error: {reason}") + } + Self::Response(reason) => { + write!(f, "Response error: {reason}") + } + Self::Write(reason) => { + write!(f, "I/O Write error: {reason}") + } + } + } +} diff --git a/src/client/response.rs b/src/client/response.rs index e9ecdf2..96ab10b 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,6 +1,35 @@ //! 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 + + pub fn from_request_async( + connection: Connection, + priority: Option, + cancellable: Option, + callback: impl FnOnce(Result) + 'static, + ) { + Meta::from_stream_async(connection.stream(), priority, cancellable, |result| { + callback(match result { + Ok(meta) => Ok(Self { connection, meta }), + Err(reason) => Err(Error::Meta(reason)), + }) + }) + } +} diff --git a/src/client/response/data/text.rs b/src/client/response/data/text.rs index f2c814b..85312d6 100644 --- a/src/client/response/data/text.rs +++ b/src/client/response/data/text.rs @@ -16,7 +16,7 @@ pub const BUFFER_MAX_SIZE: usize = 0xfffff; // 1M /// Container for text-based response data pub struct Text { - data: GString, + pub data: GString, } impl Default for Text { @@ -41,10 +41,10 @@ impl Text { } /// Create new `Self` from UTF-8 buffer - pub fn from_utf8(buffer: &[u8]) -> Result)> { + pub fn from_utf8(buffer: &[u8]) -> Result { match GString::from_utf8(buffer.into()) { Ok(data) => Ok(Self::from_string(&data)), - Err(_) => Err((Error::Decode, None)), + Err(reason) => Err(Error::Decode(reason)), } } @@ -53,7 +53,7 @@ impl Text { stream: impl IsA, priority: Option, cancellable: Option, - on_complete: impl FnOnce(Result)>) + 'static, + on_complete: impl FnOnce(Result) + 'static, ) { read_all_from_stream_async( Vec::with_capacity(BUFFER_CAPACITY), @@ -72,13 +72,6 @@ impl Text { }, ); } - - // Getters - - /// Get reference to `Self` data - pub fn data(&self) -> &GString { - &self.data - } } // Tools @@ -92,7 +85,7 @@ pub fn read_all_from_stream_async( stream: impl IsA, cancelable: Option, priority: Priority, - callback: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, + callback: impl FnOnce(Result, Error>) + 'static, ) { stream.input_stream().read_bytes_async( BUFFER_CAPACITY, @@ -107,7 +100,7 @@ pub fn read_all_from_stream_async( // Validate overflow if buffer.len() + bytes.len() > BUFFER_MAX_SIZE { - return callback(Err((Error::BufferOverflow, None))); + return callback(Err(Error::BufferOverflow)); } // Save chunks to buffer @@ -118,7 +111,7 @@ pub fn read_all_from_stream_async( // Continue bytes reading read_all_from_stream_async(buffer, stream, cancelable, priority, callback); } - Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))), + Err(reason) => callback(Err(Error::InputStreamRead(reason))), }, ); } diff --git a/src/client/response/data/text/error.rs b/src/client/response/data/text/error.rs index 3007499..5de7592 100644 --- a/src/client/response/data/text/error.rs +++ b/src/client/response/data/text/error.rs @@ -1,6 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + #[derive(Debug)] pub enum Error { BufferOverflow, - Decode, - InputStream, + Decode(std::string::FromUtf8Error), + InputStreamRead(glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::BufferOverflow => { + write!(f, "Buffer overflow") + } + Self::Decode(reason) => { + write!(f, "Decode error: {reason}") + } + Self::InputStreamRead(reason) => { + write!(f, "Input stream read error: {reason}") + } + } + } } diff --git a/src/client/response/error.rs b/src/client/response/error.rs new file mode 100644 index 0000000..9b3afb8 --- /dev/null +++ b/src/client/response/error.rs @@ -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(reason) => { + write!(f, "Meta read error: {reason}") + } + Self::Stream => { + write!(f, "I/O stream error") + } + } + } +} diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 82b5f3e..29d05bd 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -22,9 +22,9 @@ use glib::{object::IsA, Priority}; pub const MAX_LEN: usize = 0x400; // 1024 pub struct Meta { - status: Status, - data: Option, - mime: Option, + pub status: Status, + pub data: Option, + pub mime: Option, // @TODO // charset: Option, // language: Option, @@ -35,7 +35,7 @@ impl Meta { /// Create new `Self` from UTF-8 buffer /// * supports entire response or just meta slice - pub fn from_utf8(buffer: &[u8]) -> Result)> { + pub fn from_utf8(buffer: &[u8]) -> Result { // Calculate buffer length once let len = buffer.len(); @@ -46,13 +46,7 @@ impl Meta { let data = Data::from_utf8(slice); if let Err(reason) = data { - return Err(( - match reason { - data::Error::Decode => Error::DataDecode, - data::Error::Protocol => Error::DataProtocol, - }, - None, - )); + return Err(Error::Data(reason)); } // MIME @@ -60,14 +54,7 @@ impl Meta { let mime = Mime::from_utf8(slice); if let Err(reason) = mime { - return Err(( - match reason { - mime::Error::Decode => Error::MimeDecode, - mime::Error::Protocol => Error::MimeProtocol, - mime::Error::Undefined => Error::MimeUndefined, - }, - None, - )); + return Err(Error::Mime(reason)); } // Status @@ -75,14 +62,7 @@ impl Meta { let status = Status::from_utf8(slice); if let Err(reason) = status { - return Err(( - match reason { - status::Error::Decode => Error::StatusDecode, - status::Error::Protocol => Error::StatusProtocol, - status::Error::Undefined => Error::StatusUndefined, - }, - None, - )); + return Err(Error::Status(reason)); } Ok(Self { @@ -91,7 +71,7 @@ impl Meta { status: status.unwrap(), }) } - None => Err((Error::Protocol, None)), + None => Err(Error::Protocol), } } @@ -100,7 +80,7 @@ impl Meta { stream: impl IsA, priority: Option, cancellable: Option, - on_complete: impl FnOnce(Result)>) + 'static, + on_complete: impl FnOnce(Result) + 'static, ) { read_from_stream_async( Vec::with_capacity(MAX_LEN), @@ -119,20 +99,6 @@ impl Meta { }, ); } - - // Getters - - pub fn status(&self) -> &Status { - &self.status - } - - pub fn data(&self) -> &Option { - &self.data - } - - pub fn mime(&self) -> &Option { - &self.mime - } } // Tools @@ -146,7 +112,7 @@ pub fn read_from_stream_async( stream: impl IsA, cancellable: Option, priority: Priority, - on_complete: impl FnOnce(Result, (Error, Option<&str>)>) + 'static, + on_complete: impl FnOnce(Result, Error>) + 'static, ) { stream.input_stream().read_async( vec![0], @@ -156,7 +122,7 @@ pub fn read_from_stream_async( Ok((mut bytes, size)) => { // Expect valid header length if size == 0 || buffer.len() >= MAX_LEN { - return on_complete(Err((Error::Protocol, None))); + return on_complete(Err(Error::Protocol)); } // Read next byte without record @@ -181,7 +147,7 @@ pub fn read_from_stream_async( // Continue read_from_stream_async(buffer, stream, cancellable, priority, on_complete); } - Err((_, reason)) => on_complete(Err((Error::InputStream, Some(reason.message())))), + Err((data, reason)) => on_complete(Err(Error::InputStreamRead(data, reason))), }, ); } diff --git a/src/client/response/meta/data.rs b/src/client/response/meta/data.rs index 710d4f6..0a67b10 100644 --- a/src/client/response/meta/data.rs +++ b/src/client/response/meta/data.rs @@ -12,7 +12,7 @@ use glib::GString; /// * placeholder text for 10, 11 status /// * URL string for 30, 31 status pub struct Data { - value: GString, + pub value: GString, } impl Data { @@ -52,16 +52,10 @@ impl Data { false => Some(Self { value }), true => None, }), - Err(_) => Err(Error::Decode), + Err(reason) => Err(Error::Decode(reason)), } } None => Err(Error::Protocol), } } - - // Getters - - pub fn value(&self) -> &GString { - &self.value - } } diff --git a/src/client/response/meta/data/error.rs b/src/client/response/meta/data/error.rs index 125f9c6..6681812 100644 --- a/src/client/response/meta/data/error.rs +++ b/src/client/response/meta/data/error.rs @@ -1,5 +1,20 @@ +use std::fmt::{Display, Formatter, Result}; + #[derive(Debug)] pub enum Error { - Decode, + Decode(std::string::FromUtf8Error), Protocol, } + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Decode(reason) => { + write!(f, "Decode error: {reason}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + } + } +} diff --git a/src/client/response/meta/error.rs b/src/client/response/meta/error.rs index b1414eb..9688a4b 100644 --- a/src/client/response/meta/error.rs +++ b/src/client/response/meta/error.rs @@ -1,13 +1,33 @@ +use std::fmt::{Display, Formatter, Result}; + #[derive(Debug)] pub enum Error { - DataDecode, - DataProtocol, - InputStream, - MimeDecode, - MimeProtocol, - MimeUndefined, + Data(super::data::Error), + InputStreamRead(Vec, glib::Error), + Mime(super::mime::Error), Protocol, - StatusDecode, - StatusProtocol, - StatusUndefined, + Status(super::status::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Data(reason) => { + write!(f, "Data error: {reason}") + } + Self::InputStreamRead(_, reason) => { + // @TODO + write!(f, "Input stream error: {reason}") + } + Self::Mime(reason) => { + write!(f, "MIME error: {reason}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Status(reason) => { + write!(f, "Status error: {reason}") + } + } + } } diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index ec8849d..4a4f7f2 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -47,7 +47,7 @@ impl Mime { 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(_) => Err(Error::Decode), + Err(reason) => Err(Error::Decode(reason)), }, None => Err(Error::Protocol), } diff --git a/src/client/response/meta/mime/error.rs b/src/client/response/meta/mime/error.rs index 989e734..5b9eccc 100644 --- a/src/client/response/meta/mime/error.rs +++ b/src/client/response/meta/mime/error.rs @@ -1,6 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + #[derive(Debug)] pub enum Error { - Decode, + Decode(std::string::FromUtf8Error), Protocol, Undefined, } + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Decode(reason) => { + write!(f, "Decode error: {reason}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Undefined => { + write!(f, "Undefined error") + } + } + } +} diff --git a/src/client/response/meta/status.rs b/src/client/response/meta/status.rs index 1d0c719..5a5062a 100644 --- a/src/client/response/meta/status.rs +++ b/src/client/response/meta/status.rs @@ -43,7 +43,7 @@ impl Status { match buffer.get(0..2) { Some(value) => match GString::from_utf8(value.to_vec()) { Ok(string) => Self::from_string(string.as_str()), - Err(_) => Err(Error::Decode), + Err(reason) => Err(Error::Decode(reason)), }, None => Err(Error::Protocol), } diff --git a/src/client/response/meta/status/error.rs b/src/client/response/meta/status/error.rs index 989e734..5b9eccc 100644 --- a/src/client/response/meta/status/error.rs +++ b/src/client/response/meta/status/error.rs @@ -1,6 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + #[derive(Debug)] pub enum Error { - Decode, + Decode(std::string::FromUtf8Error), Protocol, Undefined, } + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Decode(reason) => { + write!(f, "Decode error: {reason}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Undefined => { + write!(f, "Undefined error") + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 26403ca..350fdd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,4 @@ pub mod client; pub mod gio; + +pub use client::Client; From 239786da6a82507dfb23af6c69cd30b7a928186f Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 15:39:37 +0200 Subject: [PATCH 121/392] update memory input stream errors handler --- src/client/response/data/text.rs | 2 +- src/client/response/data/text/error.rs | 4 ++-- src/client/response/meta.rs | 2 +- src/client/response/meta/error.rs | 4 ++-- src/gio/memory_input_stream.rs | 8 ++++---- src/gio/memory_input_stream/error.rs | 19 +++++++++++++++++-- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/client/response/data/text.rs b/src/client/response/data/text.rs index 85312d6..ba695ca 100644 --- a/src/client/response/data/text.rs +++ b/src/client/response/data/text.rs @@ -111,7 +111,7 @@ pub fn read_all_from_stream_async( // Continue bytes reading read_all_from_stream_async(buffer, stream, cancelable, priority, callback); } - Err(reason) => callback(Err(Error::InputStreamRead(reason))), + Err(reason) => callback(Err(Error::InputStream(reason))), }, ); } diff --git a/src/client/response/data/text/error.rs b/src/client/response/data/text/error.rs index 5de7592..423c19c 100644 --- a/src/client/response/data/text/error.rs +++ b/src/client/response/data/text/error.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result}; pub enum Error { BufferOverflow, Decode(std::string::FromUtf8Error), - InputStreamRead(glib::Error), + InputStream(glib::Error), } impl Display for Error { @@ -16,7 +16,7 @@ impl Display for Error { Self::Decode(reason) => { write!(f, "Decode error: {reason}") } - Self::InputStreamRead(reason) => { + Self::InputStream(reason) => { write!(f, "Input stream read error: {reason}") } } diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 29d05bd..8cca009 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -147,7 +147,7 @@ pub fn read_from_stream_async( // Continue read_from_stream_async(buffer, stream, cancellable, priority, on_complete); } - Err((data, reason)) => on_complete(Err(Error::InputStreamRead(data, reason))), + Err((data, reason)) => on_complete(Err(Error::InputStream(data, reason))), }, ); } diff --git a/src/client/response/meta/error.rs b/src/client/response/meta/error.rs index 9688a4b..246fd48 100644 --- a/src/client/response/meta/error.rs +++ b/src/client/response/meta/error.rs @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { Data(super::data::Error), - InputStreamRead(Vec, glib::Error), + InputStream(Vec, glib::Error), Mime(super::mime::Error), Protocol, Status(super::status::Error), @@ -15,7 +15,7 @@ impl Display for Error { Self::Data(reason) => { write!(f, "Data error: {reason}") } - Self::InputStreamRead(_, reason) => { + Self::InputStream(_, reason) => { // @TODO write!(f, "Input stream error: {reason}") } diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index ce7bb47..6c451d0 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -20,7 +20,7 @@ pub fn from_stream_async( bytes_in_chunk: usize, bytes_total_limit: usize, on_chunk: impl Fn((Bytes, usize)) + 'static, - on_complete: impl FnOnce(Result)>) + 'static, + on_complete: impl FnOnce(Result) + 'static, ) { read_all_from_stream_async( MemoryInputStream::new(), @@ -43,7 +43,7 @@ pub fn read_all_from_stream_async( bytes: (usize, usize, usize), callback: ( impl Fn((Bytes, usize)) + 'static, - impl FnOnce(Result)>) + 'static, + impl FnOnce(Result) + 'static, ), ) { let (on_chunk, on_complete) = callback; @@ -63,7 +63,7 @@ pub fn read_all_from_stream_async( // Validate max size if bytes_total > bytes_total_limit { - return on_complete(Err((Error::BytesTotal, None))); + return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); } // No bytes were read, end of stream @@ -85,7 +85,7 @@ pub fn read_all_from_stream_async( ); } Err(reason) => { - on_complete(Err((Error::InputStream, Some(reason.message())))); + on_complete(Err(Error::InputStream(reason))); } }, ); diff --git a/src/gio/memory_input_stream/error.rs b/src/gio/memory_input_stream/error.rs index b5cda40..515d6c9 100644 --- a/src/gio/memory_input_stream/error.rs +++ b/src/gio/memory_input_stream/error.rs @@ -1,5 +1,20 @@ +use std::fmt::{Display, Formatter, Result}; + #[derive(Debug)] pub enum Error { - BytesTotal, - InputStream, + BytesTotal(usize, usize), + InputStream(glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::BytesTotal(total, limit) => { + write!(f, "Bytes total limit reached: {total} / {limit}") + } + Self::InputStream(reason) => { + write!(f, "Input stream error: {reason}") + } + } + } } From 5737b892786564efcd6add41b434e3034f62e64e Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 15:55:39 +0200 Subject: [PATCH 122/392] delegate from_uri method to gio::network_address wrapper --- src/client.rs | 22 +++------------------- src/client/error.rs | 10 +++++++--- src/gio.rs | 1 + src/gio/network_address.rs | 24 ++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 src/gio/network_address.rs diff --git a/src/client.rs b/src/client.rs index ed70099..02824c1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,7 +11,7 @@ pub use response::Response; use gio::{ prelude::{IOStreamExt, OutputStreamExt, SocketClientExt}, - Cancellable, NetworkAddress, SocketClient, SocketProtocol, TlsCertificate, + Cancellable, SocketClient, SocketProtocol, TlsCertificate, }; use glib::{Bytes, Priority, Uri}; @@ -49,7 +49,7 @@ impl Client { certificate: Option, callback: impl Fn(Result) + 'static, ) { - match network_address_for(&uri) { + match crate::gio::network_address::from_uri(&uri, DEFAULT_PORT) { Ok(network_address) => { self.socket.connect_async( &network_address.clone(), @@ -78,29 +78,13 @@ impl Client { }, ); } - Err(reason) => callback(Err(reason)), + Err(reason) => callback(Err(Error::NetworkAddress(reason))), }; } } // Private helpers -/// [SocketConnectable](https://docs.gtk.org/gio/iface.SocketConnectable.html) / -/// [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) -fn network_address_for(uri: &Uri) -> Result { - Ok(NetworkAddress::new( - &match uri.host() { - Some(host) => host, - None => return Err(Error::Connectable(uri.to_string())), - }, - if uri.port().is_positive() { - uri.port() as u16 - } else { - DEFAULT_PORT - }, - )) -} - fn request_async( connection: Connection, query: String, diff --git a/src/client/error.rs b/src/client/error.rs index a9eb0ac..65771c0 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -2,11 +2,12 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Connectable(String), - Connection(super::connection::Error), Connect(glib::Error), + Connectable(String), + Connection(crate::client::connection::Error), + NetworkAddress(crate::gio::network_address::Error), Request(glib::Error), - Response(super::response::Error), + Response(crate::client::response::Error), Write(glib::Error), } @@ -22,6 +23,9 @@ impl Display for Error { Self::Connect(reason) => { write!(f, "Connect error: {reason}") } + Self::NetworkAddress(reason) => { + write!(f, "Network address error: {reason}") + } Self::Request(reason) => { write!(f, "Request error: {reason}") } diff --git a/src/gio.rs b/src/gio.rs index c20d929..8206018 100644 --- a/src/gio.rs +++ b/src/gio.rs @@ -1 +1,2 @@ pub mod memory_input_stream; +pub mod network_address; diff --git a/src/gio/network_address.rs b/src/gio/network_address.rs new file mode 100644 index 0000000..4869509 --- /dev/null +++ b/src/gio/network_address.rs @@ -0,0 +1,24 @@ +pub mod error; +pub use error::Error; + +use gio::NetworkAddress; +use glib::Uri; + +/// Create new valid [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) from [Uri](https://docs.gtk.org/glib/struct.Uri.html) +/// +/// Useful as: +/// * shared [SocketConnectable](https://docs.gtk.org/gio/iface.SocketConnectable.html) interface +/// * [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) record for TLS connections +pub fn from_uri(uri: &Uri, default_port: u16) -> Result { + Ok(NetworkAddress::new( + &match uri.host() { + Some(host) => host, + None => return Err(Error::Host(uri.to_string())), + }, + if uri.port().is_positive() { + uri.port() as u16 + } else { + default_port + }, + )) +} From 017a63b4e58b5878bdfeb3d102f1e6885ad10933 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 16:11:30 +0200 Subject: [PATCH 123/392] update enum names --- src/client/connection.rs | 4 ++-- src/client/connection/error.rs | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index d11f006..41c00f8 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -58,7 +58,7 @@ pub fn auth( certificate: TlsCertificate, ) -> Result { if socket_connection.is_closed() { - return Err(Error::Closed); + return Err(Error::SocketConnectionClosed); } // https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls @@ -77,6 +77,6 @@ pub fn auth( Ok(tls_client_connection) } - Err(reason) => Err(Error::Tls(reason)), + Err(reason) => Err(Error::TlsClientConnection(reason)), } } diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index aec2687..48eed23 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,15 +2,17 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Closed, - Tls(glib::Error), + SocketConnectionClosed, + TlsClientConnection(glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Closed => write!(f, "Socket connection closed"), - Self::Tls(reason) => write!(f, "Could not create TLS connection: {reason}"), + Self::SocketConnectionClosed => write!(f, "Socket connection closed"), + Self::TlsClientConnection(reason) => { + write!(f, "Could not create TLS connection: {reason}") + } } } } From cbaa4c0e815451b6e8c89da77704b3fe1f9a150c Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 16:11:54 +0200 Subject: [PATCH 124/392] fix enum name --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 41c00f8..a86a85b 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -25,7 +25,7 @@ impl Connection { certificate: Option, ) -> Result { if socket_connection.is_closed() { - return Err(Error::Closed); + return Err(Error::SocketConnectionClosed); } Ok(Self { From cc0625d920120660c5fceb8a00271315b684bdd5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 16:45:39 +0200 Subject: [PATCH 125/392] update argument types --- src/client.rs | 55 +++++++++++++++++++++++++--------------- src/client/connection.rs | 20 +++++++-------- src/client/error.rs | 8 +++--- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/client.rs b/src/client.rs index 02824c1..119fc87 100644 --- a/src/client.rs +++ b/src/client.rs @@ -60,15 +60,22 @@ impl Client { .as_ref(), move |result| match result { Ok(connection) => { - match Connection::from(network_address, connection, certificate) { + match Connection::new_for( + &connection, + certificate.as_ref(), + Some(&network_address), + ) { Ok(result) => request_async( result, uri.to_string(), match priority { - Some(priority) => priority, - None => Priority::DEFAULT, + Some(priority) => Some(priority), + None => Some(Priority::DEFAULT), + }, + match cancellable { + Some(ref cancellable) => Some(cancellable.clone()), + None => None::, }, - cancellable.unwrap(), // @TODO move |result| callback(result), ), Err(reason) => callback(Err(Error::Connection(reason))), @@ -83,30 +90,36 @@ impl Client { } } -// Private helpers - -fn request_async( +/// Make new request for constructed `Connection` +/// * callback with `Result`on success or `Error` on failure +pub fn request_async( connection: Connection, query: String, - priority: Priority, - cancellable: Cancellable, + priority: Option, + cancellable: Option, callback: impl Fn(Result) + 'static, ) { connection.stream().output_stream().write_bytes_async( &Bytes::from(format!("{query}\r\n").as_bytes()), - priority, - Some(&cancellable.clone()), + match priority { + Some(priority) => priority, + None => Priority::DEFAULT, + }, + match cancellable { + Some(ref cancellable) => Some(cancellable.clone()), + None => None::, + } + .as_ref(), move |result| match result { - Ok(_) => Response::from_request_async( - connection, - Some(priority), - Some(cancellable), - move |result| match result { - Ok(response) => callback(Ok(response)), - Err(reason) => callback(Err(Error::Response(reason))), - }, - ), - Err(reason) => callback(Err(Error::Write(reason))), + Ok(_) => { + Response::from_request_async(connection, priority, cancellable, move |result| { + callback(match result { + Ok(response) => Ok(response), + Err(reason) => Err(Error::Response(reason)), + }) + }) + } + Err(reason) => callback(Err(Error::OutputStream(reason))), }, ); } diff --git a/src/client/connection.rs b/src/client/connection.rs index a86a85b..1dede2a 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -19,10 +19,10 @@ impl Connection { // Constructors /// Create new `Self` - pub fn from( - network_address: NetworkAddress, // @TODO struct cert as sni - socket_connection: SocketConnection, - certificate: Option, + pub fn new_for( + socket_connection: &SocketConnection, + certificate: Option<&TlsCertificate>, + server_identity: Option<&NetworkAddress>, ) -> Result { if socket_connection.is_closed() { return Err(Error::SocketConnectionClosed); @@ -31,7 +31,7 @@ impl Connection { Ok(Self { socket_connection: socket_connection.clone(), tls_client_connection: match certificate { - Some(certificate) => match auth(network_address, socket_connection, certificate) { + Some(certificate) => match auth(socket_connection, certificate, server_identity) { Ok(tls_client_connection) => Some(tls_client_connection), Err(reason) => return Err(reason), }, @@ -53,19 +53,19 @@ impl Connection { // Tools pub fn auth( - server_identity: NetworkAddress, // @TODO impl IsA ? - socket_connection: SocketConnection, - certificate: TlsCertificate, + socket_connection: &SocketConnection, + certificate: &TlsCertificate, + server_identity: Option<&NetworkAddress>, ) -> Result { if socket_connection.is_closed() { return Err(Error::SocketConnectionClosed); } // https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls - match TlsClientConnection::new(&socket_connection, Some(&server_identity)) { + match TlsClientConnection::new(socket_connection, server_identity) { Ok(tls_client_connection) => { // https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates - tls_client_connection.set_certificate(&certificate); + tls_client_connection.set_certificate(certificate); // @TODO handle exceptions // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections diff --git a/src/client/error.rs b/src/client/error.rs index 65771c0..b6df128 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -6,9 +6,9 @@ pub enum Error { Connectable(String), Connection(crate::client::connection::Error), NetworkAddress(crate::gio::network_address::Error), + OutputStream(glib::Error), Request(glib::Error), Response(crate::client::response::Error), - Write(glib::Error), } impl Display for Error { @@ -26,15 +26,15 @@ impl Display for Error { Self::NetworkAddress(reason) => { write!(f, "Network address error: {reason}") } + Self::OutputStream(reason) => { + write!(f, "Output stream error: {reason}") + } Self::Request(reason) => { write!(f, "Request error: {reason}") } Self::Response(reason) => { write!(f, "Response error: {reason}") } - Self::Write(reason) => { - write!(f, "I/O Write error: {reason}") - } } } } From c7f992e1b3f4fa02be1d7091529df7625c77f552 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 16:46:34 +0200 Subject: [PATCH 126/392] implement errors handler --- src/gio/network_address/error.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/gio/network_address/error.rs diff --git a/src/gio/network_address/error.rs b/src/gio/network_address/error.rs new file mode 100644 index 0000000..762fab6 --- /dev/null +++ b/src/gio/network_address/error.rs @@ -0,0 +1,16 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Host(String), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Host(url) => { + write!(f, "Host required for {url}") + } + } + } +} From 9d240c4c37d4aa6854ce9c895b82c5b741935c51 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 16:48:22 +0200 Subject: [PATCH 127/392] fix comments --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 119fc87..ea35202 100644 --- a/src/client.rs +++ b/src/client.rs @@ -38,7 +38,7 @@ impl Client { // Actions /// Make async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), - /// callback with `Result`on success or `Error` on failure. + /// callback with `Response`on success or `Error` on failure. /// * creates new [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// * session management by Glib TLS Backend pub fn request_async( @@ -91,7 +91,7 @@ impl Client { } /// Make new request for constructed `Connection` -/// * callback with `Result`on success or `Error` on failure +/// * callback with `Response`on success or `Error` on failure pub fn request_async( connection: Connection, query: String, From 4e712260ff63a56d1c823ebf474ddcbdcf39de9a Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 18:03:22 +0200 Subject: [PATCH 128/392] add client certificate api --- src/client.rs | 2 + src/client/{connection => }/certificate.rs | 9 +++- src/client/certificate/error.rs | 55 ++++++++++++++++++++++ src/client/certificate/scope.rs | 41 ++++++++++++++++ src/client/certificate/scope/error.rs | 24 ++++++++++ src/client/connection.rs | 3 -- 6 files changed, 129 insertions(+), 5 deletions(-) rename src/client/{connection => }/certificate.rs (84%) create mode 100644 src/client/certificate/error.rs create mode 100644 src/client/certificate/scope.rs create mode 100644 src/client/certificate/scope/error.rs diff --git a/src/client.rs b/src/client.rs index ea35202..89ef9c7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,10 +1,12 @@ //! High-level client API to interact with Gemini Socket Server: //! * https://geminiprotocol.net/docs/protocol-specification.gmi +pub mod certificate; pub mod connection; pub mod error; pub mod response; +pub use certificate::Certificate; pub use connection::Connection; pub use error::Error; pub use response::Response; diff --git a/src/client/connection/certificate.rs b/src/client/certificate.rs similarity index 84% rename from src/client/connection/certificate.rs rename to src/client/certificate.rs index f33e2f4..11e8ec1 100644 --- a/src/client/connection/certificate.rs +++ b/src/client/certificate.rs @@ -8,15 +8,20 @@ use gio::{prelude::TlsCertificateExt, TlsCertificate}; use glib::DateTime; pub struct Certificate { - tls_certificate: TlsCertificate, + pub scope: Scope, + pub tls_certificate: TlsCertificate, } impl Certificate { // Constructors /// Create new `Self` - pub fn from_pem(pem: &str) -> Result { + pub fn from_pem(pem: &str, scope_url: &str) -> Result { Ok(Self { + scope: match Scope::from_url(scope_url) { + Ok(scope) => scope, + Err(reason) => return Err(Error::Scope(reason)), + }, tls_certificate: match TlsCertificate::from_pem(&pem) { Ok(tls_certificate) => { // Validate expiration time diff --git a/src/client/certificate/error.rs b/src/client/certificate/error.rs new file mode 100644 index 0000000..5dae782 --- /dev/null +++ b/src/client/certificate/error.rs @@ -0,0 +1,55 @@ +use std::fmt::{Display, Formatter, Result}; + +use glib::gformat; + +#[derive(Debug)] +pub enum Error { + DateTime, + Decode(glib::Error), + Expired(glib::DateTime), + Inactive(glib::DateTime), + Scope(crate::client::certificate::scope::Error), + ValidAfter, + ValidBefore, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::DateTime => { + write!(f, "Could not parse local `DateTime`") + } + Self::Decode(reason) => { + write!( + f, + "Could not decode TLS certificate from PEM string: {reason}" + ) + } + Self::Expired(not_valid_after) => { + write!( + f, + "Certificate expired after: {}", + match not_valid_after.format_iso8601() { + Ok(value) => value, + Err(_) => gformat!("unknown"), + } + ) + } + Self::Inactive(not_valid_before) => { + write!( + f, + "Certificate inactive before: {}", + match not_valid_before.format_iso8601() { + Ok(value) => value, + Err(_) => gformat!("unknown"), + } + ) + } + Self::Scope(reason) => { + write!(f, "Certificate inactive before: {reason}") + } + Self::ValidAfter => write!(f, "Could not get `not_valid_after` value"), + Self::ValidBefore => write!(f, "Could not get `not_valid_before` value"), + } + } +} diff --git a/src/client/certificate/scope.rs b/src/client/certificate/scope.rs new file mode 100644 index 0000000..b50b051 --- /dev/null +++ b/src/client/certificate/scope.rs @@ -0,0 +1,41 @@ +pub mod error; +pub use error::Error; + +use glib::{GString, Uri, UriFlags, UriHideFlags}; + +/// Scope implement path prefix to apply TLS authorization for +/// * https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 +pub struct Scope { + uri: Uri, +} + +impl Scope { + // Constructors + + /// Create new `Self` for given `url` string + /// * check URI parts required for valid `Scope` build + pub fn from_url(url: &str) -> Result { + match Uri::parse(url, UriFlags::NONE) { + Ok(uri) => { + if !uri.scheme().to_lowercase().contains("gemini") { + return Err(Error::Scheme); + } + + if uri.host().is_none() { + return Err(Error::Host); + } + + Ok(Self { uri }) + } + Err(reason) => Err(Error::Uri(reason)), + } + } + + // Getters + + /// Get `Scope` string match [Specification](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) + pub fn to_string(&self) -> GString { + self.uri + .to_string_partial(UriHideFlags::QUERY | UriHideFlags::FRAGMENT) + } +} diff --git a/src/client/certificate/scope/error.rs b/src/client/certificate/scope/error.rs new file mode 100644 index 0000000..ea32f87 --- /dev/null +++ b/src/client/certificate/scope/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Host, + Scheme, + Uri(glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Host => { + write!(f, "Host required") + } + Self::Scheme => { + write!(f, "Scope does not match `gemini`") + } + Self::Uri(reason) => { + write!(f, "Could not parse URI: {reason}") + } + } + } +} diff --git a/src/client/connection.rs b/src/client/connection.rs index 1dede2a..09f5bda 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,7 +1,4 @@ -pub mod certificate; pub mod error; - -pub use certificate::Certificate; pub use error::Error; use gio::{ From ffda3d27e34bf6e9fe73fd57c22f7099141e27f0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 18:17:25 +0200 Subject: [PATCH 129/392] add to_network_address method --- src/client/certificate/scope.rs | 12 ++++++++++++ src/client/certificate/scope/error.rs | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/src/client/certificate/scope.rs b/src/client/certificate/scope.rs index b50b051..5c25cc2 100644 --- a/src/client/certificate/scope.rs +++ b/src/client/certificate/scope.rs @@ -1,6 +1,8 @@ pub mod error; pub use error::Error; +use crate::client::DEFAULT_PORT; +use gio::NetworkAddress; use glib::{GString, Uri, UriFlags, UriHideFlags}; /// Scope implement path prefix to apply TLS authorization for @@ -38,4 +40,14 @@ impl Scope { self.uri .to_string_partial(UriHideFlags::QUERY | UriHideFlags::FRAGMENT) } + + /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) + /// implement [SocketConnectable](https://docs.gtk.org/gio/iface.SocketConnectable.html) interface + /// * useful as [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) in TLS context + pub fn to_network_address(&self) -> Result { + match crate::gio::network_address::from_uri(&self.uri, DEFAULT_PORT) { + Ok(network_address) => Ok(network_address), + Err(reason) => Err(Error::NetworkAddress(reason)), + } + } } diff --git a/src/client/certificate/scope/error.rs b/src/client/certificate/scope/error.rs index ea32f87..f41c3b3 100644 --- a/src/client/certificate/scope/error.rs +++ b/src/client/certificate/scope/error.rs @@ -3,6 +3,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { Host, + NetworkAddress(crate::gio::network_address::Error), Scheme, Uri(glib::Error), } @@ -13,6 +14,9 @@ impl Display for Error { Self::Host => { write!(f, "Host required") } + Self::NetworkAddress(reason) => { + write!(f, "Could not parse network address: {reason}") + } Self::Scheme => { write!(f, "Scope does not match `gemini`") } From b9be89ca0fe153affd18f8f69c293e8e63abe3ec Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 18:21:16 +0200 Subject: [PATCH 130/392] set DEFAULT_PORT cost as global --- src/client.rs | 3 +-- src/client/certificate/scope.rs | 2 +- src/lib.rs | 6 ++++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 89ef9c7..8d8d924 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,7 +17,6 @@ use gio::{ }; use glib::{Bytes, Priority, Uri}; -pub const DEFAULT_PORT: u16 = 1965; pub const DEFAULT_TIMEOUT: u32 = 10; pub struct Client { @@ -51,7 +50,7 @@ impl Client { certificate: Option, callback: impl Fn(Result) + 'static, ) { - match crate::gio::network_address::from_uri(&uri, DEFAULT_PORT) { + match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { Ok(network_address) => { self.socket.connect_async( &network_address.clone(), diff --git a/src/client/certificate/scope.rs b/src/client/certificate/scope.rs index 5c25cc2..0d5cd20 100644 --- a/src/client/certificate/scope.rs +++ b/src/client/certificate/scope.rs @@ -1,7 +1,7 @@ pub mod error; pub use error::Error; -use crate::client::DEFAULT_PORT; +use crate::DEFAULT_PORT; use gio::NetworkAddress; use glib::{GString, Uri, UriFlags, UriHideFlags}; diff --git a/src/lib.rs b/src/lib.rs index 350fdd6..868a4f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,10 @@ pub mod client; pub mod gio; +// Main API + pub use client::Client; + +// Global defaults + +pub const DEFAULT_PORT: u16 = 1965; From 5e20e5a925d00e1d847d1bef0f1d9b747cdc8a5b Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 18:25:03 +0200 Subject: [PATCH 131/392] update comments --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8d8d924..9f21975 100644 --- a/src/client.rs +++ b/src/client.rs @@ -39,7 +39,7 @@ impl Client { // Actions /// Make async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), - /// callback with `Response`on success or `Error` on failure. + /// callback with new `Response`on success or `Error` on failure. /// * creates new [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// * session management by Glib TLS Backend pub fn request_async( @@ -92,7 +92,7 @@ impl Client { } /// Make new request for constructed `Connection` -/// * callback with `Response`on success or `Error` on failure +/// * callback with new `Response`on success or `Error` on failure pub fn request_async( connection: Connection, query: String, From 8f2820b171bf5affb332973fb5b3bfc18a620d2d Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 18:33:34 +0200 Subject: [PATCH 132/392] require v2_70 --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e27248d..538c997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,7 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" version = "0.20.4" -# currently not required -# features = ["v2_70"] +features = ["v2_70"] [dependencies.glib] package = "glib" From c61164e666403c1e4299e2535fdd7e8caf161dc3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 18:52:39 +0200 Subject: [PATCH 133/392] toggle set_tls for guest sessions --- src/client.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client.rs b/src/client.rs index 9f21975..4bef71c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -50,6 +50,10 @@ impl Client { certificate: Option, callback: impl Fn(Result) + 'static, ) { + // Toggle socket mode + // * guest sessions will not work without! + self.socket.set_tls(certificate.is_none()); + match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { Ok(network_address) => { self.socket.connect_async( From b1b35e059bdecd5f94a3b7f09a0f448dc7d651f8 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 19:56:36 +0200 Subject: [PATCH 134/392] add guest certificates validation --- src/client.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4bef71c..76028f2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,10 +12,11 @@ pub use error::Error; pub use response::Response; use gio::{ - prelude::{IOStreamExt, OutputStreamExt, SocketClientExt}, - Cancellable, SocketClient, SocketProtocol, TlsCertificate, + prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsConnectionExt}, + Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificate, + TlsClientConnection, }; -use glib::{Bytes, Priority, Uri}; +use glib::{object::Cast, Bytes, Priority, Uri}; pub const DEFAULT_TIMEOUT: u32 = 10; @@ -28,11 +29,28 @@ impl Client { /// Create new `Self` pub fn new() -> Self { + // Init new socket let socket = SocketClient::new(); + // Setup initial configuration for Gemini Protocol socket.set_protocol(SocketProtocol::Tcp); socket.set_timeout(DEFAULT_TIMEOUT); + // Connect events + socket.connect_event(move |_, event, _, stream| { + // This condition have effect only for guest TLS connections + // * for user certificates validation, use `Connection` impl + if event == SocketClientEvent::TlsHandshaking { + // Begin guest certificate validation + stream + .unwrap() + .dynamic_cast_ref::() + .unwrap() + .connect_accept_certificate(|_, _, _| true); // @TODO + } + }); + + // Done Self { socket } } From 1fbcfcfff00f88d6c94449cf70bc45fa7437fada Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 20:06:44 +0200 Subject: [PATCH 135/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 76028f2..0b5cf00 100644 --- a/src/client.rs +++ b/src/client.rs @@ -39,7 +39,7 @@ impl Client { // Connect events socket.connect_event(move |_, event, _, stream| { // This condition have effect only for guest TLS connections - // * for user certificates validation, use `Connection` impl + // * for user certificates validation, use `Connection` auth if event == SocketClientEvent::TlsHandshaking { // Begin guest certificate validation stream From 2e7df281a9173eac590fe701749c077265e8ad79 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 22:20:30 +0200 Subject: [PATCH 136/392] update comment --- src/client/certificate/scope.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/certificate/scope.rs b/src/client/certificate/scope.rs index 0d5cd20..88963d3 100644 --- a/src/client/certificate/scope.rs +++ b/src/client/certificate/scope.rs @@ -14,8 +14,8 @@ pub struct Scope { impl Scope { // Constructors - /// Create new `Self` for given `url` string - /// * check URI parts required for valid `Scope` build + /// Create new `Self` for given `url` + /// * external validator MAY decline `Certificate` if `Scope` defined out of protocol range pub fn from_url(url: &str) -> Result { match Uri::parse(url, UriFlags::NONE) { Ok(uri) => { From 6c88eedd33494b927636976d894c8fde0f623ecd Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 22:23:55 +0200 Subject: [PATCH 137/392] update comment --- src/client/certificate/scope.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/certificate/scope.rs b/src/client/certificate/scope.rs index 88963d3..1b80784 100644 --- a/src/client/certificate/scope.rs +++ b/src/client/certificate/scope.rs @@ -6,7 +6,8 @@ use gio::NetworkAddress; use glib::{GString, Uri, UriFlags, UriHideFlags}; /// Scope implement path prefix to apply TLS authorization for -/// * https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 +/// * external validator MAY decline `Certificate` if `Scope` defined out of protocol range +/// * [read more](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) pub struct Scope { uri: Uri, } @@ -15,7 +16,6 @@ impl Scope { // Constructors /// Create new `Self` for given `url` - /// * external validator MAY decline `Certificate` if `Scope` defined out of protocol range pub fn from_url(url: &str) -> Result { match Uri::parse(url, UriFlags::NONE) { Ok(uri) => { From 9ebd1e03f62cf5bf0b1f27c1bfd941623f326138 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 22:54:18 +0200 Subject: [PATCH 138/392] use certificate wrapper --- src/client.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0b5cf00..13cbc5b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,8 +13,7 @@ pub use response::Response; use gio::{ prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsConnectionExt}, - Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificate, - TlsClientConnection, + Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsClientConnection, }; use glib::{object::Cast, Bytes, Priority, Uri}; @@ -65,7 +64,7 @@ impl Client { uri: Uri, priority: Option, cancellable: Option, - certificate: Option, + certificate: Option, callback: impl Fn(Result) + 'static, ) { // Toggle socket mode @@ -85,7 +84,10 @@ impl Client { Ok(connection) => { match Connection::new_for( &connection, - certificate.as_ref(), + match certificate { + Some(ref certificate) => Some(&certificate.tls_certificate), + None => None, + }, Some(&network_address), ) { Ok(result) => request_async( From 2079bb11676342f816d38ead6c17266104806b64 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 28 Nov 2024 01:58:17 +0200 Subject: [PATCH 139/392] rename methods --- src/client.rs | 4 ++-- src/client/connection.rs | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index 13cbc5b..3c8a6c1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -38,7 +38,7 @@ impl Client { // Connect events socket.connect_event(move |_, event, _, stream| { // This condition have effect only for guest TLS connections - // * for user certificates validation, use `Connection` auth + // * for user certificates validation, use `Connection` impl if event == SocketClientEvent::TlsHandshaking { // Begin guest certificate validation stream @@ -82,7 +82,7 @@ impl Client { .as_ref(), move |result| match result { Ok(connection) => { - match Connection::new_for( + match Connection::new_wrap( &connection, match certificate { Some(ref certificate) => Some(&certificate.tls_certificate), diff --git a/src/client/connection.rs b/src/client/connection.rs index 09f5bda..7e9cb5f 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -16,7 +16,7 @@ impl Connection { // Constructors /// Create new `Self` - pub fn new_for( + pub fn new_wrap( socket_connection: &SocketConnection, certificate: Option<&TlsCertificate>, server_identity: Option<&NetworkAddress>, @@ -28,10 +28,13 @@ impl Connection { Ok(Self { socket_connection: socket_connection.clone(), tls_client_connection: match certificate { - Some(certificate) => match auth(socket_connection, certificate, server_identity) { - Ok(tls_client_connection) => Some(tls_client_connection), - Err(reason) => return Err(reason), - }, + Some(certificate) => { + match new_tls_client_connection(socket_connection, certificate, server_identity) + { + Ok(tls_client_connection) => Some(tls_client_connection), + Err(reason) => return Err(reason), + } + } None => None, }, }) @@ -49,7 +52,7 @@ impl Connection { // Tools -pub fn auth( +pub fn new_tls_client_connection( socket_connection: &SocketConnection, certificate: &TlsCertificate, server_identity: Option<&NetworkAddress>, From e437d35acf1857f138a9062c2ba670f850044c6e Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 28 Nov 2024 02:21:15 +0200 Subject: [PATCH 140/392] use native TlsCertificate --- src/client.rs | 12 ++---- src/client/certificate.rs | 57 --------------------------- src/client/certificate/error.rs | 55 -------------------------- src/client/certificate/scope.rs | 53 ------------------------- src/client/certificate/scope/error.rs | 28 ------------- 5 files changed, 4 insertions(+), 201 deletions(-) delete mode 100644 src/client/certificate.rs delete mode 100644 src/client/certificate/error.rs delete mode 100644 src/client/certificate/scope.rs delete mode 100644 src/client/certificate/scope/error.rs diff --git a/src/client.rs b/src/client.rs index 3c8a6c1..78ac177 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,19 +1,18 @@ //! High-level client API to interact with Gemini Socket Server: //! * https://geminiprotocol.net/docs/protocol-specification.gmi -pub mod certificate; pub mod connection; pub mod error; pub mod response; -pub use certificate::Certificate; pub use connection::Connection; pub use error::Error; pub use response::Response; use gio::{ prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsConnectionExt}, - Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsClientConnection, + Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificate, + TlsClientConnection, }; use glib::{object::Cast, Bytes, Priority, Uri}; @@ -64,7 +63,7 @@ impl Client { uri: Uri, priority: Option, cancellable: Option, - certificate: Option, + certificate: Option, callback: impl Fn(Result) + 'static, ) { // Toggle socket mode @@ -84,10 +83,7 @@ impl Client { Ok(connection) => { match Connection::new_wrap( &connection, - match certificate { - Some(ref certificate) => Some(&certificate.tls_certificate), - None => None, - }, + certificate.as_ref(), Some(&network_address), ) { Ok(result) => request_async( diff --git a/src/client/certificate.rs b/src/client/certificate.rs deleted file mode 100644 index 11e8ec1..0000000 --- a/src/client/certificate.rs +++ /dev/null @@ -1,57 +0,0 @@ -pub mod error; -pub mod scope; - -pub use error::Error; -pub use scope::Scope; - -use gio::{prelude::TlsCertificateExt, TlsCertificate}; -use glib::DateTime; - -pub struct Certificate { - pub scope: Scope, - pub tls_certificate: TlsCertificate, -} - -impl Certificate { - // Constructors - - /// Create new `Self` - pub fn from_pem(pem: &str, scope_url: &str) -> Result { - Ok(Self { - scope: match Scope::from_url(scope_url) { - Ok(scope) => scope, - Err(reason) => return Err(Error::Scope(reason)), - }, - tls_certificate: match TlsCertificate::from_pem(&pem) { - Ok(tls_certificate) => { - // Validate expiration time - match DateTime::now_local() { - Ok(now_local) => { - match tls_certificate.not_valid_after() { - Some(not_valid_after) => { - if now_local > not_valid_after { - return Err(Error::Expired(not_valid_after)); - } - } - None => return Err(Error::ValidAfter), - } - match tls_certificate.not_valid_before() { - Some(not_valid_before) => { - if now_local < not_valid_before { - return Err(Error::Inactive(not_valid_before)); - } - } - None => return Err(Error::ValidBefore), - } - } - Err(_) => return Err(Error::DateTime), - } - - // Success - tls_certificate - } - Err(reason) => return Err(Error::Decode(reason)), - }, - }) - } -} diff --git a/src/client/certificate/error.rs b/src/client/certificate/error.rs deleted file mode 100644 index 5dae782..0000000 --- a/src/client/certificate/error.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -use glib::gformat; - -#[derive(Debug)] -pub enum Error { - DateTime, - Decode(glib::Error), - Expired(glib::DateTime), - Inactive(glib::DateTime), - Scope(crate::client::certificate::scope::Error), - ValidAfter, - ValidBefore, -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::DateTime => { - write!(f, "Could not parse local `DateTime`") - } - Self::Decode(reason) => { - write!( - f, - "Could not decode TLS certificate from PEM string: {reason}" - ) - } - Self::Expired(not_valid_after) => { - write!( - f, - "Certificate expired after: {}", - match not_valid_after.format_iso8601() { - Ok(value) => value, - Err(_) => gformat!("unknown"), - } - ) - } - Self::Inactive(not_valid_before) => { - write!( - f, - "Certificate inactive before: {}", - match not_valid_before.format_iso8601() { - Ok(value) => value, - Err(_) => gformat!("unknown"), - } - ) - } - Self::Scope(reason) => { - write!(f, "Certificate inactive before: {reason}") - } - Self::ValidAfter => write!(f, "Could not get `not_valid_after` value"), - Self::ValidBefore => write!(f, "Could not get `not_valid_before` value"), - } - } -} diff --git a/src/client/certificate/scope.rs b/src/client/certificate/scope.rs deleted file mode 100644 index 1b80784..0000000 --- a/src/client/certificate/scope.rs +++ /dev/null @@ -1,53 +0,0 @@ -pub mod error; -pub use error::Error; - -use crate::DEFAULT_PORT; -use gio::NetworkAddress; -use glib::{GString, Uri, UriFlags, UriHideFlags}; - -/// Scope implement path prefix to apply TLS authorization for -/// * external validator MAY decline `Certificate` if `Scope` defined out of protocol range -/// * [read more](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) -pub struct Scope { - uri: Uri, -} - -impl Scope { - // Constructors - - /// Create new `Self` for given `url` - pub fn from_url(url: &str) -> Result { - match Uri::parse(url, UriFlags::NONE) { - Ok(uri) => { - if !uri.scheme().to_lowercase().contains("gemini") { - return Err(Error::Scheme); - } - - if uri.host().is_none() { - return Err(Error::Host); - } - - Ok(Self { uri }) - } - Err(reason) => Err(Error::Uri(reason)), - } - } - - // Getters - - /// Get `Scope` string match [Specification](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) - pub fn to_string(&self) -> GString { - self.uri - .to_string_partial(UriHideFlags::QUERY | UriHideFlags::FRAGMENT) - } - - /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) - /// implement [SocketConnectable](https://docs.gtk.org/gio/iface.SocketConnectable.html) interface - /// * useful as [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) in TLS context - pub fn to_network_address(&self) -> Result { - match crate::gio::network_address::from_uri(&self.uri, DEFAULT_PORT) { - Ok(network_address) => Ok(network_address), - Err(reason) => Err(Error::NetworkAddress(reason)), - } - } -} diff --git a/src/client/certificate/scope/error.rs b/src/client/certificate/scope/error.rs deleted file mode 100644 index f41c3b3..0000000 --- a/src/client/certificate/scope/error.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Host, - NetworkAddress(crate::gio::network_address::Error), - Scheme, - Uri(glib::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::Host => { - write!(f, "Host required") - } - Self::NetworkAddress(reason) => { - write!(f, "Could not parse network address: {reason}") - } - Self::Scheme => { - write!(f, "Scope does not match `gemini`") - } - Self::Uri(reason) => { - write!(f, "Could not parse URI: {reason}") - } - } - } -} From 1c50fadfde1acd1172e7228ec5dd2f5b5a8d2490 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 28 Nov 2024 02:31:50 +0200 Subject: [PATCH 141/392] get connection ownership on wrap --- src/client.rs | 6 +++--- src/client/connection.rs | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index 78ac177..4b09094 100644 --- a/src/client.rs +++ b/src/client.rs @@ -82,9 +82,9 @@ impl Client { move |result| match result { Ok(connection) => { match Connection::new_wrap( - &connection, - certificate.as_ref(), - Some(&network_address), + connection, + certificate, + Some(network_address), ) { Ok(result) => request_async( result, diff --git a/src/client/connection.rs b/src/client/connection.rs index 7e9cb5f..137a247 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -17,9 +17,9 @@ impl Connection { /// Create new `Self` pub fn new_wrap( - socket_connection: &SocketConnection, - certificate: Option<&TlsCertificate>, - server_identity: Option<&NetworkAddress>, + socket_connection: SocketConnection, + certificate: Option, + server_identity: Option, ) -> Result { if socket_connection.is_closed() { return Err(Error::SocketConnectionClosed); @@ -29,8 +29,11 @@ impl Connection { socket_connection: socket_connection.clone(), tls_client_connection: match certificate { Some(certificate) => { - match new_tls_client_connection(socket_connection, certificate, server_identity) - { + match new_tls_client_connection( + &socket_connection, + &certificate, + server_identity.as_ref(), + ) { Ok(tls_client_connection) => Some(tls_client_connection), Err(reason) => return Err(reason), } From 16ed3efef0c15c2b448ac356bb42a4457d059f7e Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 28 Nov 2024 21:40:21 +0200 Subject: [PATCH 142/392] implement close method, add comments --- src/client/connection.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 137a247..7eb1b11 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -3,7 +3,7 @@ pub use error::Error; use gio::{ prelude::{IOStreamExt, TlsConnectionExt}, - IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, + Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, }; use glib::object::{Cast, IsA}; @@ -43,8 +43,27 @@ impl Connection { }) } + // Actions + + /// Close owned [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + /// and [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) if active + pub fn close(&self, cancellable: Option<&Cancellable>) { + if let Some(ref tls_client_connection) = self.tls_client_connection { + if !tls_client_connection.is_closed() { + tls_client_connection.close(cancellable); + } + } + if !self.socket_connection.is_closed() { + self.socket_connection.close(cancellable); + } + } + // Getters + /// Upcast [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + /// for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) + /// or [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) (if available) + /// * wanted to keep `Connection` active in async I/O context pub fn stream(&self) -> impl IsA { match self.tls_client_connection.clone() { Some(tls_client_connection) => tls_client_connection.upcast::(), From f4cb0c3bcc07ebe1a36fff46105777e2cce89b38 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 28 Nov 2024 23:27:53 +0200 Subject: [PATCH 143/392] handle errors --- src/client/connection.rs | 11 ++++++++--- src/client/connection/error.rs | 6 +++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 7eb1b11..3bc80d9 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -47,15 +47,20 @@ impl Connection { /// Close owned [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// and [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) if active - pub fn close(&self, cancellable: Option<&Cancellable>) { + pub fn close(&self, cancellable: Option<&Cancellable>) -> Result<(), Error> { if let Some(ref tls_client_connection) = self.tls_client_connection { if !tls_client_connection.is_closed() { - tls_client_connection.close(cancellable); + if let Err(reason) = tls_client_connection.close(cancellable) { + return Err(Error::TlsClientConnection(reason)); + } } } if !self.socket_connection.is_closed() { - self.socket_connection.close(cancellable); + if let Err(reason) = self.socket_connection.close(cancellable) { + return Err(Error::SocketConnection(reason)); + } } + Ok(()) } // Getters diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 48eed23..41e0efa 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -3,6 +3,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { SocketConnectionClosed, + SocketConnection(glib::Error), TlsClientConnection(glib::Error), } @@ -10,8 +11,11 @@ impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { Self::SocketConnectionClosed => write!(f, "Socket connection closed"), + Self::SocketConnection(reason) => { + write!(f, "Socket connection error: {reason}") + } Self::TlsClientConnection(reason) => { - write!(f, "Could not create TLS connection: {reason}") + write!(f, "TLS client connection error: {reason}") } } } From c4c173f6cf66d2f9d05163a49310f6e6ab4039c4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 29 Nov 2024 17:54:37 +0200 Subject: [PATCH 144/392] implement session update on certificate change in runtime --- src/client.rs | 120 +++++++++++++++++++++++++++++------------ src/client/response.rs | 5 +- src/client/session.rs | 24 +++++++++ 3 files changed, 113 insertions(+), 36 deletions(-) create mode 100644 src/client/session.rs diff --git a/src/client.rs b/src/client.rs index 4b09094..4e812de 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,13 +4,17 @@ pub mod connection; pub mod error; pub mod response; +pub mod session; + +use std::rc::Rc; pub use connection::Connection; pub use error::Error; pub use response::Response; +pub use session::Session; use gio::{ - prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsCertificateExt, TlsConnectionExt}, Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificate, TlsClientConnection, }; @@ -19,6 +23,7 @@ use glib::{object::Cast, Bytes, Priority, Uri}; pub const DEFAULT_TIMEOUT: u32 = 10; pub struct Client { + session: Rc, pub socket: SocketClient, } @@ -49,15 +54,18 @@ impl Client { }); // Done - Self { socket } + Self { + session: Rc::new(Session::new()), + socket, + } } // Actions - /// Make async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), + /// Make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), /// callback with new `Response`on success or `Error` on failure. - /// * creates new [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - /// * session management by Glib TLS Backend + /// * call this method ignore default session resumption by Glib TLS backend, + /// implement certificate change ability in application runtime pub fn request_async( &self, uri: Uri, @@ -70,51 +78,95 @@ impl Client { // * guest sessions will not work without! self.socket.set_tls(certificate.is_none()); - match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { - Ok(network_address) => { - self.socket.connect_async( + // Update previous session available for this request + match self.update_session(&uri, certificate.as_ref()) { + Ok(()) => match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { + Ok(network_address) => self.socket.connect_async( &network_address.clone(), match cancellable { Some(ref cancellable) => Some(cancellable.clone()), None => None::, } .as_ref(), - move |result| match result { - Ok(connection) => { - match Connection::new_wrap( - connection, - certificate, - Some(network_address), - ) { - Ok(result) => request_async( - result, - uri.to_string(), - match priority { - Some(priority) => Some(priority), - None => Some(Priority::DEFAULT), - }, - match cancellable { - Some(ref cancellable) => Some(cancellable.clone()), - None => None::, - }, - move |result| callback(result), - ), - Err(reason) => callback(Err(Error::Connection(reason))), + { + let session = self.session.clone(); + move |result| match result { + Ok(connection) => { + match Connection::new_wrap( + connection, + certificate, + Some(network_address), + ) { + Ok(connection) => { + // Wrap connection to shared reference clone semantics + let connection = Rc::new(connection); + + // Update session record + session.update(uri.to_string(), connection.clone()); + + // Begin new request + request_async( + connection, + uri.to_string(), + match priority { + Some(priority) => Some(priority), + None => Some(Priority::DEFAULT), + }, + match cancellable { + Some(ref cancellable) => Some(cancellable.clone()), + None => None::, + }, + move |result| callback(result), + ) + } + Err(reason) => callback(Err(Error::Connection(reason))), + } } + Err(reason) => callback(Err(Error::Connect(reason))), } - Err(reason) => callback(Err(Error::Connect(reason))), }, - ); + ), + Err(reason) => callback(Err(Error::NetworkAddress(reason))), + }, + Err(reason) => callback(Err(reason)), + } + } + + /// Update existing session for given request + pub fn update_session( + &self, + uri: &Uri, + certificate: Option<&TlsCertificate>, + ) -> Result<(), Error> { + if let Some(connection) = self.session.get(&uri.to_string()) { + // Check connection contain TLS authorization + if let Some(ref tls_client_connection) = connection.tls_client_connection { + if let Some(new) = certificate { + // Get previous certificate + if let Some(ref old) = tls_client_connection.certificate() { + if !new.is_same(old) { + // Prevent session resumption + // Glib backend restore session in runtime with old certificate + // @TODO keep in mind, until better solution found for TLS 1.3 + println!("{:?}", tls_client_connection.handshake(Cancellable::NONE)); + } + } + } } - Err(reason) => callback(Err(Error::NetworkAddress(reason))), - }; + + // Close connection if active yet + if let Err(reason) = connection.close(Cancellable::NONE) { + return Err(Error::Connection(reason)); + } + } + Ok(()) } } /// Make new request for constructed `Connection` /// * callback with new `Response`on success or `Error` on failure pub fn request_async( - connection: Connection, + connection: Rc, query: String, priority: Option, cancellable: Option, diff --git a/src/client/response.rs b/src/client/response.rs index 96ab10b..b646165 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -10,9 +10,10 @@ pub use meta::Meta; use super::Connection; use gio::Cancellable; use glib::Priority; +use std::rc::Rc; pub struct Response { - pub connection: Connection, + pub connection: Rc, pub meta: Meta, } @@ -20,7 +21,7 @@ impl Response { // Constructors pub fn from_request_async( - connection: Connection, + connection: Rc, priority: Option, cancellable: Option, callback: impl FnOnce(Result) + 'static, diff --git a/src/client/session.rs b/src/client/session.rs new file mode 100644 index 0000000..6f3f1f6 --- /dev/null +++ b/src/client/session.rs @@ -0,0 +1,24 @@ +use super::Connection; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +/// Request sessions holder for `Client` object +/// * useful to keep connections open and / or validate TLS certificate updates in runtime +pub struct Session { + index: RefCell>>, +} + +impl Session { + pub fn new() -> Self { + Self { + index: RefCell::new(HashMap::new()), + } + } + + pub fn get(&self, request: &str) -> Option> { + self.index.borrow().get(request).cloned() + } + + pub fn update(&self, request: String, connection: Rc) -> Option> { + self.index.borrow_mut().insert(request, connection) + } +} From b3e9bf239ca5debb08f503573fe9e753dac1721c Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 01:48:33 +0200 Subject: [PATCH 145/392] add tls_client_connection, rehandshake methods --- src/client/connection.rs | 27 +++++++++++++++++++++++++++ src/client/connection/error.rs | 16 ++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 3bc80d9..18741c4 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -10,6 +10,7 @@ use glib::object::{Cast, IsA}; pub struct Connection { pub socket_connection: SocketConnection, pub tls_client_connection: Option, + pub server_identity: Option, } impl Connection { @@ -26,6 +27,7 @@ impl Connection { } Ok(Self { + server_identity: server_identity.clone(), socket_connection: socket_connection.clone(), tls_client_connection: match certificate { Some(certificate) => { @@ -75,6 +77,31 @@ impl Connection { None => self.socket_connection.clone().upcast::(), } } + + pub fn tls_client_connection(&self) -> Result { + match self.tls_client_connection.clone() { + // User session + Some(tls_client_connection) => Ok(tls_client_connection), + // Guest session + None => { + // Create new wrapper to interact `TlsClientConnection` API + match TlsClientConnection::new( + self.stream().as_ref(), + self.server_identity.as_ref(), + ) { + Ok(tls_client_connection) => Ok(tls_client_connection), + Err(reason) => Err(Error::TlsClientConnection(reason)), + } + } + } + } + + pub fn rehandshake(&self) -> Result<(), Error> { + match self.tls_client_connection()?.handshake(Cancellable::NONE) { + Ok(()) => Ok(()), + Err(reason) => Err(Error::Rehandshake(reason)), + } + } } // Tools diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 41e0efa..5e3f47e 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,20 +2,24 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - SocketConnectionClosed, + Rehandshake(glib::Error), SocketConnection(glib::Error), + SocketConnectionClosed, TlsClientConnection(glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::SocketConnectionClosed => write!(f, "Socket connection closed"), - Self::SocketConnection(reason) => { - write!(f, "Socket connection error: {reason}") + Self::Rehandshake(e) => { + write!(f, "Rehandshake error: {e}") } - Self::TlsClientConnection(reason) => { - write!(f, "TLS client connection error: {reason}") + Self::SocketConnectionClosed => write!(f, "Socket connection closed"), + Self::SocketConnection(e) => { + write!(f, "Socket connection error: {e}") + } + Self::TlsClientConnection(e) => { + write!(f, "TLS client connection error: {e}") } } } From c3a76472d288179c918751800a1d8c82b1dc9b53 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 01:49:50 +0200 Subject: [PATCH 146/392] update session update detection --- src/client.rs | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4e812de..c2c3f4b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -80,6 +80,7 @@ impl Client { // Update previous session available for this request match self.update_session(&uri, certificate.as_ref()) { + // Begin new connection Ok(()) => match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { Ok(network_address) => self.socket.connect_async( &network_address.clone(), @@ -101,7 +102,7 @@ impl Client { // Wrap connection to shared reference clone semantics let connection = Rc::new(connection); - // Update session record + // Update session session.update(uri.to_string(), connection.clone()); // Begin new request @@ -140,16 +141,31 @@ impl Client { ) -> Result<(), Error> { if let Some(connection) = self.session.get(&uri.to_string()) { // Check connection contain TLS authorization - if let Some(ref tls_client_connection) = connection.tls_client_connection { - if let Some(new) = certificate { - // Get previous certificate - if let Some(ref old) = tls_client_connection.certificate() { - if !new.is_same(old) { - // Prevent session resumption - // Glib backend restore session in runtime with old certificate - // @TODO keep in mind, until better solution found for TLS 1.3 - println!("{:?}", tls_client_connection.handshake(Cancellable::NONE)); + match connection.tls_client_connection { + Some(ref tls_client_connection) => { + match certificate { + Some(new) => { + // Get previous certificate + if let Some(ref old) = tls_client_connection.certificate() { + // User -> User + if !new.is_same(old) { + // Prevent session resumption + // Glib backend restore session in runtime with old certificate + // @TODO keep in mind, until better solution found for TLS 1.3 + println!("{:?}", connection.rehandshake()); + } + } } + None => { + // User -> Guest + println!("{:?}", connection.rehandshake()); + } + } + } + None => { + // Guest -> User + if certificate.is_some() { + println!("{:?}", connection.rehandshake()); } } } From 7db362aadc9b854ecf5c1cbf2d5da4ccaf470997 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 01:59:40 +0200 Subject: [PATCH 147/392] add default implementation --- src/client.rs | 6 ++++++ src/client/session.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/client.rs b/src/client.rs index c2c3f4b..50770d8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -27,6 +27,12 @@ pub struct Client { pub socket: SocketClient, } +impl Default for Client { + fn default() -> Self { + Self::new() + } +} + impl Client { // Constructors diff --git a/src/client/session.rs b/src/client/session.rs index 6f3f1f6..1f7c422 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -7,6 +7,12 @@ pub struct Session { index: RefCell>>, } +impl Default for Session { + fn default() -> Self { + Self::new() + } +} + impl Session { pub fn new() -> Self { Self { From 2db7d77f4357780f93b275fc4e7589be5fa9ddc9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 02:01:15 +0200 Subject: [PATCH 148/392] replace the closure with the function itself --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 50770d8..3b27cf3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -123,7 +123,7 @@ impl Client { Some(ref cancellable) => Some(cancellable.clone()), None => None::, }, - move |result| callback(result), + callback, ) } Err(reason) => callback(Err(Error::Connection(reason))), From ab9c7f44004334e9eba5ff768d533a491a092538 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:15:57 +0200 Subject: [PATCH 149/392] move session update method to struct implementation --- src/client.rs | 74 ++++++++----------------------------- src/client/error.rs | 28 ++++++++------ src/client/session.rs | 64 ++++++++++++++++++++++++++++++-- src/client/session/error.rs | 16 ++++++++ 4 files changed, 109 insertions(+), 73 deletions(-) create mode 100644 src/client/session/error.rs diff --git a/src/client.rs b/src/client.rs index 3b27cf3..b071614 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,7 +14,7 @@ pub use response::Response; pub use session::Session; use gio::{ - prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsCertificateExt, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsConnectionExt}, Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificate, TlsClientConnection, }; @@ -84,9 +84,11 @@ impl Client { // * guest sessions will not work without! self.socket.set_tls(certificate.is_none()); - // Update previous session available for this request - match self.update_session(&uri, certificate.as_ref()) { + // Update previous session if available for this `uri`, force rehandshake on certificate change + match self.session.update(&uri, certificate.as_ref()) { // Begin new connection + // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid + // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) Ok(()) => match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { Ok(network_address) => self.socket.connect_async( &network_address.clone(), @@ -99,17 +101,18 @@ impl Client { let session = self.session.clone(); move |result| match result { Ok(connection) => { + // Wrap required connection dependencies into the struct holder match Connection::new_wrap( connection, certificate, Some(network_address), ) { Ok(connection) => { - // Wrap connection to shared reference clone semantics + // Wrap to shared reference support clone semantics let connection = Rc::new(connection); - // Update session - session.update(uri.to_string(), connection.clone()); + // Renew session + session.set(uri.to_string(), connection.clone()); // Begin new request request_async( @@ -123,66 +126,21 @@ impl Client { Some(ref cancellable) => Some(cancellable.clone()), None => None::, }, - callback, + callback, // callback with response ) } - Err(reason) => callback(Err(Error::Connection(reason))), + Err(e) => callback(Err(Error::Connection(e))), } } - Err(reason) => callback(Err(Error::Connect(reason))), + Err(e) => callback(Err(Error::Connect(e))), } }, ), - Err(reason) => callback(Err(Error::NetworkAddress(reason))), + Err(e) => callback(Err(Error::NetworkAddress(e))), }, - Err(reason) => callback(Err(reason)), + Err(e) => callback(Err(Error::Session(e))), } } - - /// Update existing session for given request - pub fn update_session( - &self, - uri: &Uri, - certificate: Option<&TlsCertificate>, - ) -> Result<(), Error> { - if let Some(connection) = self.session.get(&uri.to_string()) { - // Check connection contain TLS authorization - match connection.tls_client_connection { - Some(ref tls_client_connection) => { - match certificate { - Some(new) => { - // Get previous certificate - if let Some(ref old) = tls_client_connection.certificate() { - // User -> User - if !new.is_same(old) { - // Prevent session resumption - // Glib backend restore session in runtime with old certificate - // @TODO keep in mind, until better solution found for TLS 1.3 - println!("{:?}", connection.rehandshake()); - } - } - } - None => { - // User -> Guest - println!("{:?}", connection.rehandshake()); - } - } - } - None => { - // Guest -> User - if certificate.is_some() { - println!("{:?}", connection.rehandshake()); - } - } - } - - // Close connection if active yet - if let Err(reason) = connection.close(Cancellable::NONE) { - return Err(Error::Connection(reason)); - } - } - Ok(()) - } } /// Make new request for constructed `Connection` @@ -210,11 +168,11 @@ pub fn request_async( Response::from_request_async(connection, priority, cancellable, move |result| { callback(match result { Ok(response) => Ok(response), - Err(reason) => Err(Error::Response(reason)), + Err(e) => Err(Error::Response(e)), }) }) } - Err(reason) => callback(Err(Error::OutputStream(reason))), + Err(e) => callback(Err(Error::OutputStream(e))), }, ); } diff --git a/src/client/error.rs b/src/client/error.rs index b6df128..bbb159a 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -9,6 +9,7 @@ pub enum Error { OutputStream(glib::Error), Request(glib::Error), Response(crate::client::response::Error), + Session(crate::client::session::Error), } impl Display for Error { @@ -17,23 +18,26 @@ impl Display for Error { Self::Connectable(uri) => { write!(f, "Could not create connectable address for {uri}") } - Self::Connection(reason) => { - write!(f, "Connection error: {reason}") + Self::Connection(e) => { + write!(f, "Connection error: {e}") } - Self::Connect(reason) => { - write!(f, "Connect error: {reason}") + Self::Connect(e) => { + write!(f, "Connect error: {e}") } - Self::NetworkAddress(reason) => { - write!(f, "Network address error: {reason}") + Self::NetworkAddress(e) => { + write!(f, "Network address error: {e}") } - Self::OutputStream(reason) => { - write!(f, "Output stream error: {reason}") + Self::OutputStream(e) => { + write!(f, "Output stream error: {e}") } - Self::Request(reason) => { - write!(f, "Request error: {reason}") + Self::Request(e) => { + write!(f, "Request error: {e}") } - Self::Response(reason) => { - write!(f, "Response error: {reason}") + Self::Response(e) => { + write!(f, "Response error: {e}") + } + Self::Session(e) => { + write!(f, "Session error: {e}") } } } diff --git a/src/client/session.rs b/src/client/session.rs index 1f7c422..c7d93ba 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -1,8 +1,16 @@ +mod error; +pub use error::Error; + use super::Connection; +use gio::{ + prelude::{TlsCertificateExt, TlsConnectionExt}, + Cancellable, TlsCertificate, +}; +use glib::Uri; use std::{cell::RefCell, collections::HashMap, rc::Rc}; -/// Request sessions holder for `Client` object -/// * useful to keep connections open and / or validate TLS certificate updates in runtime +/// Request sessions holder +/// * useful to keep connections open in async context and / or validate TLS certificate updates in runtime pub struct Session { index: RefCell>>, } @@ -24,7 +32,57 @@ impl Session { self.index.borrow().get(request).cloned() } - pub fn update(&self, request: String, connection: Rc) -> Option> { + pub fn set(&self, request: String, connection: Rc) -> Option> { self.index.borrow_mut().insert(request, connection) } + + /// Update existing session for given [Uri](https://docs.gtk.org/glib/struct.Uri.html) + /// and [TlsCertificate](https://docs.gtk.org/gio/class.TlsCertificate.html) + /// * force rehandshake on user certificate was changed in runtime (ignore default session resumption by Glib TLS backend implementation) + /// * close previous connection match `Uri` if not closed yet + pub fn update(&self, uri: &Uri, certificate: Option<&TlsCertificate>) -> Result<(), Error> { + if let Some(connection) = self.get(&uri.to_string()) { + match connection.tls_client_connection { + // User certificate session + Some(ref tls_client_connection) => { + match certificate { + Some(new) => { + // Get previous certificate + if let Some(ref old) = tls_client_connection.certificate() { + // User -> User + if !new.is_same(old) { + rehandshake(connection.as_ref()); + } + } + } + // User -> Guest + None => rehandshake(connection.as_ref()), + } + } + // Guest + None => { + // Guest -> User + if certificate.is_some() { + rehandshake(connection.as_ref()) + } + } + } + + // Close connection if active yet + if let Err(reason) = connection.close(Cancellable::NONE) { + return Err(Error::Connection(reason)); + } + } + Ok(()) + } +} + +// Tools + +// Applies re-handshake to connection to prevent default session resumption +// on user certificate change in runtime +pub fn rehandshake(connection: &Connection) { + if let Err(e) = connection.rehandshake() { + println!("warning: {e}"); // @TODO keep in mind until solution for TLS 1.3 + } } diff --git a/src/client/session/error.rs b/src/client/session/error.rs new file mode 100644 index 0000000..a974d1a --- /dev/null +++ b/src/client/session/error.rs @@ -0,0 +1,16 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Connection(crate::client::connection::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Connection(e) => { + write!(f, "Connection error: {e}") + } + } + } +} From a0fa799163ace937e48290b478593b9374843a4a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:16:40 +0200 Subject: [PATCH 150/392] update comment --- src/client/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/session.rs b/src/client/session.rs index c7d93ba..9e7d1ad 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -79,7 +79,7 @@ impl Session { // Tools -// Applies re-handshake to connection to prevent default session resumption +// Applies re-handshake to `Connection` to prevent default session resumption // on user certificate change in runtime pub fn rehandshake(connection: &Connection) { if let Err(e) = connection.rehandshake() { From d0a26f429b248e2d9a45a2bfd0172c175f05a77a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:17:08 +0200 Subject: [PATCH 151/392] reorder namespaces --- src/client.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index b071614..a9a7d19 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,8 +6,6 @@ pub mod error; pub mod response; pub mod session; -use std::rc::Rc; - pub use connection::Connection; pub use error::Error; pub use response::Response; @@ -19,6 +17,7 @@ use gio::{ TlsClientConnection, }; use glib::{object::Cast, Bytes, Priority, Uri}; +use std::rc::Rc; pub const DEFAULT_TIMEOUT: u32 = 10; From 9f9e0e0ea32908d45398025185a39b9e6b95cbfd Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:32:37 +0200 Subject: [PATCH 152/392] update comment --- src/client.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index a9a7d19..da6c179 100644 --- a/src/client.rs +++ b/src/client.rs @@ -68,8 +68,12 @@ impl Client { // Actions /// Make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), - /// callback with new `Response`on success or `Error` on failure. - /// * call this method ignore default session resumption by Glib TLS backend, + /// callback with new `Response`on success or `Error` on failure + /// + /// * method does not close new `Connection` created, hold it in `Session`, + /// expects from user manual `Response` handle with close act on complete + /// * if new request match same `uri`, method auto-close previous connection, renew `Session` + /// * method ignores default session resumption provided by Glib TLS backend, /// implement certificate change ability in application runtime pub fn request_async( &self, From 745b8a67867d70ee6b383aca09751303271ca088 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:35:00 +0200 Subject: [PATCH 153/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index da6c179..450d265 100644 --- a/src/client.rs +++ b/src/client.rs @@ -129,7 +129,7 @@ impl Client { Some(ref cancellable) => Some(cancellable.clone()), None => None::, }, - callback, // callback with response + callback, // result ) } Err(e) => callback(Err(Error::Connection(e))), From 71135fac5828d793cd4fbb29d4ea1b435f00e460 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:39:18 +0200 Subject: [PATCH 154/392] add comment --- src/client.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client.rs b/src/client.rs index 450d265..e33bf4f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -21,6 +21,10 @@ use std::rc::Rc; pub const DEFAULT_TIMEOUT: u32 = 10; +/// Main point where connect external crate +/// +/// Includes high-level API for session-safe interaction with +/// [Gemini protocol](https://geminiprotocol.net) socket server pub struct Client { session: Rc, pub socket: SocketClient, From 911539eab98660f72e971d389464a68804c80f22 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:39:57 +0200 Subject: [PATCH 155/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index e33bf4f..a93957f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -23,7 +23,7 @@ pub const DEFAULT_TIMEOUT: u32 = 10; /// Main point where connect external crate /// -/// Includes high-level API for session-safe interaction with +/// Provides high-level API for session-safe interaction with /// [Gemini protocol](https://geminiprotocol.net) socket server pub struct Client { session: Rc, From 16971f93216918425e5e9e9baf1e2099e8be0e01 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:40:53 +0200 Subject: [PATCH 156/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index a93957f..f145088 100644 --- a/src/client.rs +++ b/src/client.rs @@ -24,7 +24,7 @@ pub const DEFAULT_TIMEOUT: u32 = 10; /// Main point where connect external crate /// /// Provides high-level API for session-safe interaction with -/// [Gemini protocol](https://geminiprotocol.net) socket server +/// [Gemini](https://geminiprotocol.net) socket server pub struct Client { session: Rc, pub socket: SocketClient, From c57ac0de83223bf1a1ea76565b5cd48c369a6eb6 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:42:28 +0200 Subject: [PATCH 157/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index f145088..d1b17c5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -150,7 +150,7 @@ impl Client { } } -/// Make new request for constructed `Connection` +/// Middle-level method to make new request to `Connection` /// * callback with new `Response`on success or `Error` on failure pub fn request_async( connection: Rc, From 809e54b8874f580706804783cbfd2e274f0c8456 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 03:44:14 +0200 Subject: [PATCH 158/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index d1b17c5..f988854 100644 --- a/src/client.rs +++ b/src/client.rs @@ -71,7 +71,7 @@ impl Client { // Actions - /// Make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), + /// High-level method make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), /// callback with new `Response`on success or `Error` on failure /// /// * method does not close new `Connection` created, hold it in `Session`, From a06e4e9effdd99e12b9bcae3758d1f3e3962a957 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 04:05:31 +0200 Subject: [PATCH 159/392] enshort common error name --- src/client/connection.rs | 16 ++++++++-------- src/client/response.rs | 2 +- src/client/response/data/text.rs | 6 +++--- src/client/response/data/text/error.rs | 8 ++++---- src/client/response/error.rs | 4 ++-- src/client/response/meta.rs | 16 ++++++++-------- src/client/response/meta/data.rs | 2 +- src/client/response/meta/data/error.rs | 4 ++-- src/client/response/meta/error.rs | 16 ++++++++-------- src/client/response/meta/mime.rs | 2 +- src/client/response/meta/mime/error.rs | 4 ++-- src/client/response/meta/status.rs | 2 +- src/client/response/meta/status/error.rs | 4 ++-- src/client/session.rs | 4 ++-- src/gio/memory_input_stream.rs | 4 ++-- src/gio/memory_input_stream/error.rs | 4 ++-- 16 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 18741c4..b0a8a23 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -37,7 +37,7 @@ impl Connection { server_identity.as_ref(), ) { Ok(tls_client_connection) => Some(tls_client_connection), - Err(reason) => return Err(reason), + Err(e) => return Err(e), } } None => None, @@ -52,14 +52,14 @@ impl Connection { pub fn close(&self, cancellable: Option<&Cancellable>) -> Result<(), Error> { if let Some(ref tls_client_connection) = self.tls_client_connection { if !tls_client_connection.is_closed() { - if let Err(reason) = tls_client_connection.close(cancellable) { - return Err(Error::TlsClientConnection(reason)); + if let Err(e) = tls_client_connection.close(cancellable) { + return Err(Error::TlsClientConnection(e)); } } } if !self.socket_connection.is_closed() { - if let Err(reason) = self.socket_connection.close(cancellable) { - return Err(Error::SocketConnection(reason)); + if let Err(e) = self.socket_connection.close(cancellable) { + return Err(Error::SocketConnection(e)); } } Ok(()) @@ -90,7 +90,7 @@ impl Connection { self.server_identity.as_ref(), ) { Ok(tls_client_connection) => Ok(tls_client_connection), - Err(reason) => Err(Error::TlsClientConnection(reason)), + Err(e) => Err(Error::TlsClientConnection(e)), } } } @@ -99,7 +99,7 @@ impl Connection { pub fn rehandshake(&self) -> Result<(), Error> { match self.tls_client_connection()?.handshake(Cancellable::NONE) { Ok(()) => Ok(()), - Err(reason) => Err(Error::Rehandshake(reason)), + Err(e) => Err(Error::Rehandshake(e)), } } } @@ -131,6 +131,6 @@ pub fn new_tls_client_connection( Ok(tls_client_connection) } - Err(reason) => Err(Error::TlsClientConnection(reason)), + Err(e) => Err(Error::TlsClientConnection(e)), } } diff --git a/src/client/response.rs b/src/client/response.rs index b646165..345b262 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -29,7 +29,7 @@ impl Response { Meta::from_stream_async(connection.stream(), priority, cancellable, |result| { callback(match result { Ok(meta) => Ok(Self { connection, meta }), - Err(reason) => Err(Error::Meta(reason)), + Err(e) => Err(Error::Meta(e)), }) }) } diff --git a/src/client/response/data/text.rs b/src/client/response/data/text.rs index ba695ca..4de4654 100644 --- a/src/client/response/data/text.rs +++ b/src/client/response/data/text.rs @@ -44,7 +44,7 @@ impl Text { pub fn from_utf8(buffer: &[u8]) -> Result { match GString::from_utf8(buffer.into()) { Ok(data) => Ok(Self::from_string(&data)), - Err(reason) => Err(Error::Decode(reason)), + Err(e) => Err(Error::Decode(e)), } } @@ -68,7 +68,7 @@ impl Text { }, |result| match result { Ok(buffer) => on_complete(Self::from_utf8(&buffer)), - Err(reason) => on_complete(Err(reason)), + Err(e) => on_complete(Err(e)), }, ); } @@ -111,7 +111,7 @@ pub fn read_all_from_stream_async( // Continue bytes reading read_all_from_stream_async(buffer, stream, cancelable, priority, callback); } - Err(reason) => callback(Err(Error::InputStream(reason))), + Err(e) => callback(Err(Error::InputStream(e))), }, ); } diff --git a/src/client/response/data/text/error.rs b/src/client/response/data/text/error.rs index 423c19c..4a853aa 100644 --- a/src/client/response/data/text/error.rs +++ b/src/client/response/data/text/error.rs @@ -13,11 +13,11 @@ impl Display for Error { Self::BufferOverflow => { write!(f, "Buffer overflow") } - Self::Decode(reason) => { - write!(f, "Decode error: {reason}") + Self::Decode(e) => { + write!(f, "Decode error: {e}") } - Self::InputStream(reason) => { - write!(f, "Input stream read error: {reason}") + Self::InputStream(e) => { + write!(f, "Input stream read error: {e}") } } } diff --git a/src/client/response/error.rs b/src/client/response/error.rs index 9b3afb8..9834190 100644 --- a/src/client/response/error.rs +++ b/src/client/response/error.rs @@ -9,8 +9,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Meta(reason) => { - write!(f, "Meta read error: {reason}") + Self::Meta(e) => { + write!(f, "Meta read error: {e}") } Self::Stream => { write!(f, "I/O stream error") diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 8cca009..42bd4dd 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -45,24 +45,24 @@ impl Meta { // Parse data let data = Data::from_utf8(slice); - if let Err(reason) = data { - return Err(Error::Data(reason)); + if let Err(e) = data { + return Err(Error::Data(e)); } // MIME let mime = Mime::from_utf8(slice); - if let Err(reason) = mime { - return Err(Error::Mime(reason)); + if let Err(e) = mime { + return Err(Error::Mime(e)); } // Status let status = Status::from_utf8(slice); - if let Err(reason) = status { - return Err(Error::Status(reason)); + if let Err(e) = status { + return Err(Error::Status(e)); } Ok(Self { @@ -95,7 +95,7 @@ impl Meta { }, |result| match result { Ok(buffer) => on_complete(Self::from_utf8(&buffer)), - Err(reason) => on_complete(Err(reason)), + Err(e) => on_complete(Err(e)), }, ); } @@ -147,7 +147,7 @@ pub fn read_from_stream_async( // Continue read_from_stream_async(buffer, stream, cancellable, priority, on_complete); } - Err((data, reason)) => on_complete(Err(Error::InputStream(data, reason))), + Err((data, e)) => on_complete(Err(Error::InputStream(data, e))), }, ); } diff --git a/src/client/response/meta/data.rs b/src/client/response/meta/data.rs index 0a67b10..e86af61 100644 --- a/src/client/response/meta/data.rs +++ b/src/client/response/meta/data.rs @@ -52,7 +52,7 @@ impl Data { false => Some(Self { value }), true => None, }), - Err(reason) => Err(Error::Decode(reason)), + Err(e) => Err(Error::Decode(e)), } } None => Err(Error::Protocol), diff --git a/src/client/response/meta/data/error.rs b/src/client/response/meta/data/error.rs index 6681812..49455cd 100644 --- a/src/client/response/meta/data/error.rs +++ b/src/client/response/meta/data/error.rs @@ -9,8 +9,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Decode(reason) => { - write!(f, "Decode error: {reason}") + Self::Decode(e) => { + write!(f, "Decode error: {e}") } Self::Protocol => { write!(f, "Protocol error") diff --git a/src/client/response/meta/error.rs b/src/client/response/meta/error.rs index 246fd48..55abb24 100644 --- a/src/client/response/meta/error.rs +++ b/src/client/response/meta/error.rs @@ -12,21 +12,21 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Data(reason) => { - write!(f, "Data error: {reason}") + Self::Data(e) => { + write!(f, "Data error: {e}") } - Self::InputStream(_, reason) => { + Self::InputStream(_, e) => { // @TODO - write!(f, "Input stream error: {reason}") + write!(f, "Input stream error: {e}") } - Self::Mime(reason) => { - write!(f, "MIME error: {reason}") + Self::Mime(e) => { + write!(f, "MIME error: {e}") } Self::Protocol => { write!(f, "Protocol error") } - Self::Status(reason) => { - write!(f, "Status error: {reason}") + Self::Status(e) => { + write!(f, "Status error: {e}") } } } diff --git a/src/client/response/meta/mime.rs b/src/client/response/meta/mime.rs index 4a4f7f2..c86d627 100644 --- a/src/client/response/meta/mime.rs +++ b/src/client/response/meta/mime.rs @@ -47,7 +47,7 @@ impl Mime { 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(reason) => Err(Error::Decode(reason)), + Err(e) => Err(Error::Decode(e)), }, None => Err(Error::Protocol), } diff --git a/src/client/response/meta/mime/error.rs b/src/client/response/meta/mime/error.rs index 5b9eccc..4b66ed2 100644 --- a/src/client/response/meta/mime/error.rs +++ b/src/client/response/meta/mime/error.rs @@ -10,8 +10,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Decode(reason) => { - write!(f, "Decode error: {reason}") + Self::Decode(e) => { + write!(f, "Decode error: {e}") } Self::Protocol => { write!(f, "Protocol error") diff --git a/src/client/response/meta/status.rs b/src/client/response/meta/status.rs index 5a5062a..c0079f4 100644 --- a/src/client/response/meta/status.rs +++ b/src/client/response/meta/status.rs @@ -43,7 +43,7 @@ impl Status { match buffer.get(0..2) { Some(value) => match GString::from_utf8(value.to_vec()) { Ok(string) => Self::from_string(string.as_str()), - Err(reason) => Err(Error::Decode(reason)), + Err(e) => Err(Error::Decode(e)), }, None => Err(Error::Protocol), } diff --git a/src/client/response/meta/status/error.rs b/src/client/response/meta/status/error.rs index 5b9eccc..4b66ed2 100644 --- a/src/client/response/meta/status/error.rs +++ b/src/client/response/meta/status/error.rs @@ -10,8 +10,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Decode(reason) => { - write!(f, "Decode error: {reason}") + Self::Decode(e) => { + write!(f, "Decode error: {e}") } Self::Protocol => { write!(f, "Protocol error") diff --git a/src/client/session.rs b/src/client/session.rs index 9e7d1ad..4b138f6 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -69,8 +69,8 @@ impl Session { } // Close connection if active yet - if let Err(reason) = connection.close(Cancellable::NONE) { - return Err(Error::Connection(reason)); + if let Err(e) = connection.close(Cancellable::NONE) { + return Err(Error::Connection(e)); } } Ok(()) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 6c451d0..fc175e0 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -84,8 +84,8 @@ pub fn read_all_from_stream_async( (on_chunk, on_complete), ); } - Err(reason) => { - on_complete(Err(Error::InputStream(reason))); + Err(e) => { + on_complete(Err(Error::InputStream(e))); } }, ); diff --git a/src/gio/memory_input_stream/error.rs b/src/gio/memory_input_stream/error.rs index 515d6c9..673906c 100644 --- a/src/gio/memory_input_stream/error.rs +++ b/src/gio/memory_input_stream/error.rs @@ -12,8 +12,8 @@ impl Display for Error { Self::BytesTotal(total, limit) => { write!(f, "Bytes total limit reached: {total} / {limit}") } - Self::InputStream(reason) => { - write!(f, "Input stream error: {reason}") + Self::InputStream(e) => { + write!(f, "Input stream error: {e}") } } } From 873489df29f3cc42f9555188377f6a8a04a0f169 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 04:17:27 +0200 Subject: [PATCH 160/392] reorder actions, add comments --- src/client/connection.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index b0a8a23..677ee52 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -65,6 +65,16 @@ impl Connection { Ok(()) } + /// Request force handshake for `Self` connection + /// * useful for certificate change in runtime + pub fn rehandshake(&self) -> Result<(), Error> { + match self.tls_client_connection()?.handshake(Cancellable::NONE) { + // @TODO shared `Cancellable` + Ok(()) => Ok(()), + Err(e) => Err(Error::Rehandshake(e)), + } + } + // Getters /// Upcast [IOStream](https://docs.gtk.org/gio/class.IOStream.html) @@ -78,6 +88,8 @@ impl Connection { } } + /// Get [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) for `Self` + /// * compatible with user and guest sessions pub fn tls_client_connection(&self) -> Result { match self.tls_client_connection.clone() { // User session @@ -95,13 +107,6 @@ impl Connection { } } } - - pub fn rehandshake(&self) -> Result<(), Error> { - match self.tls_client_connection()?.handshake(Cancellable::NONE) { - Ok(()) => Ok(()), - Err(e) => Err(Error::Rehandshake(e)), - } - } } // Tools From 9e3176ca003ef3cd2a39b7211519f467e312038b Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 04:18:31 +0200 Subject: [PATCH 161/392] update comment --- src/client/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/session.rs b/src/client/session.rs index 4b138f6..34af1ea 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -79,8 +79,8 @@ impl Session { // Tools -// Applies re-handshake to `Connection` to prevent default session resumption -// on user certificate change in runtime +/// Applies re-handshake to `Connection` +/// to prevent default session resumption on user certificate change in runtime pub fn rehandshake(connection: &Connection) { if let Err(e) = connection.rehandshake() { println!("warning: {e}"); // @TODO keep in mind until solution for TLS 1.3 From 8618b315705f0591f3e60a4034110133df21a4bd Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 04:24:47 +0200 Subject: [PATCH 162/392] remove extra construction --- src/client.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index f988854..2f22e4e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -125,14 +125,8 @@ impl Client { request_async( connection, uri.to_string(), - match priority { - Some(priority) => Some(priority), - None => Some(Priority::DEFAULT), - }, - match cancellable { - Some(ref cancellable) => Some(cancellable.clone()), - None => None::, - }, + priority, + cancellable, callback, // result ) } From 653960c1ab0c8961b20de4dfe01aec48b2a69ed0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 04:32:51 +0200 Subject: [PATCH 163/392] update comment --- src/client/connection.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/connection.rs b/src/client/connection.rs index 677ee52..94d5a00 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -67,6 +67,7 @@ impl Connection { /// Request force handshake for `Self` connection /// * useful for certificate change in runtime + /// * support guest and user sessions pub fn rehandshake(&self) -> Result<(), Error> { match self.tls_client_connection()?.handshake(Cancellable::NONE) { // @TODO shared `Cancellable` From 1dfaf682673dbf6d576a740f4371a72658e699cb Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 04:35:02 +0200 Subject: [PATCH 164/392] rename enum option --- src/client/connection.rs | 4 ++-- src/client/connection/error.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 94d5a00..b9f799e 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -23,7 +23,7 @@ impl Connection { server_identity: Option, ) -> Result { if socket_connection.is_closed() { - return Err(Error::SocketConnectionClosed); + return Err(Error::Closed); } Ok(Self { @@ -118,7 +118,7 @@ pub fn new_tls_client_connection( server_identity: Option<&NetworkAddress>, ) -> Result { if socket_connection.is_closed() { - return Err(Error::SocketConnectionClosed); + return Err(Error::Closed); } // https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 5e3f47e..87131f7 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,19 +2,19 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { + Closed, Rehandshake(glib::Error), SocketConnection(glib::Error), - SocketConnectionClosed, TlsClientConnection(glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::Closed => write!(f, "Connection closed"), Self::Rehandshake(e) => { write!(f, "Rehandshake error: {e}") } - Self::SocketConnectionClosed => write!(f, "Socket connection closed"), Self::SocketConnection(e) => { write!(f, "Socket connection error: {e}") } From c779ca37882e0b7efaca91f9c82611b743d09b4e Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 05:08:45 +0200 Subject: [PATCH 165/392] add shared cancellable holder --- src/client.rs | 1 + src/client/connection.rs | 17 +++++++++++------ src/client/session.rs | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2f22e4e..0a8b8ac 100644 --- a/src/client.rs +++ b/src/client.rs @@ -113,6 +113,7 @@ impl Client { connection, certificate, Some(network_address), + cancellable.clone(), ) { Ok(connection) => { // Wrap to shared reference support clone semantics diff --git a/src/client/connection.rs b/src/client/connection.rs index b9f799e..2072422 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -8,9 +8,10 @@ use gio::{ use glib::object::{Cast, IsA}; pub struct Connection { + pub cancellable: Option, + pub server_identity: Option, pub socket_connection: SocketConnection, pub tls_client_connection: Option, - pub server_identity: Option, } impl Connection { @@ -21,12 +22,14 @@ impl Connection { socket_connection: SocketConnection, certificate: Option, server_identity: Option, + cancellable: Option, ) -> Result { if socket_connection.is_closed() { return Err(Error::Closed); } Ok(Self { + cancellable, server_identity: server_identity.clone(), socket_connection: socket_connection.clone(), tls_client_connection: match certificate { @@ -49,16 +52,16 @@ impl Connection { /// Close owned [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// and [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) if active - pub fn close(&self, cancellable: Option<&Cancellable>) -> Result<(), Error> { + pub fn close(&self) -> Result<(), Error> { if let Some(ref tls_client_connection) = self.tls_client_connection { if !tls_client_connection.is_closed() { - if let Err(e) = tls_client_connection.close(cancellable) { + if let Err(e) = tls_client_connection.close(self.cancellable.as_ref()) { return Err(Error::TlsClientConnection(e)); } } } if !self.socket_connection.is_closed() { - if let Err(e) = self.socket_connection.close(cancellable) { + if let Err(e) = self.socket_connection.close(self.cancellable.as_ref()) { return Err(Error::SocketConnection(e)); } } @@ -69,8 +72,10 @@ impl Connection { /// * useful for certificate change in runtime /// * support guest and user sessions pub fn rehandshake(&self) -> Result<(), Error> { - match self.tls_client_connection()?.handshake(Cancellable::NONE) { - // @TODO shared `Cancellable` + match self + .tls_client_connection()? + .handshake(self.cancellable.as_ref()) + { Ok(()) => Ok(()), Err(e) => Err(Error::Rehandshake(e)), } diff --git a/src/client/session.rs b/src/client/session.rs index 34af1ea..aea904a 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -4,7 +4,7 @@ pub use error::Error; use super::Connection; use gio::{ prelude::{TlsCertificateExt, TlsConnectionExt}, - Cancellable, TlsCertificate, + TlsCertificate, }; use glib::Uri; use std::{cell::RefCell, collections::HashMap, rc::Rc}; @@ -69,7 +69,7 @@ impl Session { } // Close connection if active yet - if let Err(e) = connection.close(Cancellable::NONE) { + if let Err(e) = connection.close() { return Err(Error::Connection(e)); } } From ed68c2201036618f3cbea9f367221599aabeb1fa Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 05:26:14 +0200 Subject: [PATCH 166/392] add comment --- src/client/connection.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/connection.rs b/src/client/connection.rs index 2072422..f9b779e 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -61,6 +61,7 @@ impl Connection { } } if !self.socket_connection.is_closed() { + // @TODO duplicated condition? if let Err(e) = self.socket_connection.close(self.cancellable.as_ref()) { return Err(Error::SocketConnection(e)); } From 2e6cdb000baff81c2e399e5b86fc94c66522d608 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 05:27:16 +0200 Subject: [PATCH 167/392] update comment --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index f9b779e..3ddc810 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -103,7 +103,7 @@ impl Connection { Some(tls_client_connection) => Ok(tls_client_connection), // Guest session None => { - // Create new wrapper to interact `TlsClientConnection` API + // Create new wrapper for `IOStream` to interact it `TlsClientConnection` API match TlsClientConnection::new( self.stream().as_ref(), self.server_identity.as_ref(), From e86a5568635626c4e7734fe4e1efd84484023723 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 05:28:41 +0200 Subject: [PATCH 168/392] update comment --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 3ddc810..ceae4d4 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -69,7 +69,7 @@ impl Connection { Ok(()) } - /// Request force handshake for `Self` connection + /// Request force handshake for `Self` /// * useful for certificate change in runtime /// * support guest and user sessions pub fn rehandshake(&self) -> Result<(), Error> { From 0ab6f978154c1c8aa875c87c0a21e70598f99ae4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 05:31:48 +0200 Subject: [PATCH 169/392] update comment --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index ceae4d4..a0fc598 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -96,7 +96,7 @@ impl Connection { } /// Get [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) for `Self` - /// * compatible with user and guest sessions + /// * compatible with both user and guest connection types pub fn tls_client_connection(&self) -> Result { match self.tls_client_connection.clone() { // User session From 36569da73bc0418139b2bb1db0ab831c9d72c47e Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 05:32:49 +0200 Subject: [PATCH 170/392] update comment --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index a0fc598..22bf4d6 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -103,7 +103,7 @@ impl Connection { Some(tls_client_connection) => Ok(tls_client_connection), // Guest session None => { - // Create new wrapper for `IOStream` to interact it `TlsClientConnection` API + // Create new wrapper for `IOStream` to interact `TlsClientConnection` API match TlsClientConnection::new( self.stream().as_ref(), self.server_identity.as_ref(), From a715e7632a9cfd89e06bafcba4dc2ed2633e5ccd Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 05:38:14 +0200 Subject: [PATCH 171/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 0a8b8ac..cb4eb70 100644 --- a/src/client.rs +++ b/src/client.rs @@ -91,7 +91,7 @@ impl Client { // * guest sessions will not work without! self.socket.set_tls(certificate.is_none()); - // Update previous session if available for this `uri`, force rehandshake on certificate change + // Update previous session if available for this `uri`, does force rehandshake on certificate change match self.session.update(&uri, certificate.as_ref()) { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid From fbfd7a2c676f28a9a77cdee4b321e244fe864ecb Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 05:38:46 +0200 Subject: [PATCH 172/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index cb4eb70..942fb17 100644 --- a/src/client.rs +++ b/src/client.rs @@ -91,7 +91,7 @@ impl Client { // * guest sessions will not work without! self.socket.set_tls(certificate.is_none()); - // Update previous session if available for this `uri`, does force rehandshake on certificate change + // Update previous session if available for this `uri`, does force rehandshake on `certificate` change match self.session.update(&uri, certificate.as_ref()) { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid From 79f219ba76775a39e2b68252e2e389ce4c90fbe6 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 16:45:02 +0200 Subject: [PATCH 173/392] disable TlsClientConnection close to prevent rehandshake failure on user certificate change in runtime --- src/client/connection.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 22bf4d6..b923a4c 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -53,15 +53,16 @@ impl Connection { /// Close owned [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// and [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) if active pub fn close(&self) -> Result<(), Error> { + /* Do not close `TlsClientConnection` as wanted for re-handshake + on user certificate change in runtime! @TODO if let Some(ref tls_client_connection) = self.tls_client_connection { if !tls_client_connection.is_closed() { if let Err(e) = tls_client_connection.close(self.cancellable.as_ref()) { return Err(Error::TlsClientConnection(e)); } } - } + } */ if !self.socket_connection.is_closed() { - // @TODO duplicated condition? if let Err(e) = self.socket_connection.close(self.cancellable.as_ref()) { return Err(Error::SocketConnection(e)); } From 6ee60e9d9df5d6d3079aae1aa7420b81bd077561 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 16:55:43 +0200 Subject: [PATCH 174/392] handle SocketConnection close errors, remove deprecated implementation --- src/client/connection.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index b923a4c..b322d27 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -51,23 +51,11 @@ impl Connection { // Actions /// Close owned [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - /// and [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) if active pub fn close(&self) -> Result<(), Error> { - /* Do not close `TlsClientConnection` as wanted for re-handshake - on user certificate change in runtime! @TODO - if let Some(ref tls_client_connection) = self.tls_client_connection { - if !tls_client_connection.is_closed() { - if let Err(e) = tls_client_connection.close(self.cancellable.as_ref()) { - return Err(Error::TlsClientConnection(e)); - } - } - } */ - if !self.socket_connection.is_closed() { - if let Err(e) = self.socket_connection.close(self.cancellable.as_ref()) { - return Err(Error::SocketConnection(e)); - } + match self.socket_connection.close(self.cancellable.as_ref()) { + Ok(()) => Ok(()), + Err(e) => Err(Error::SocketConnection(e)), } - Ok(()) } /// Request force handshake for `Self` From cdf35db0d6d98168f638af9da9ee1eff0dd727bf Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 17:26:03 +0200 Subject: [PATCH 175/392] implement cancel action --- src/client/connection.rs | 11 ++++++++++- src/client/connection/error.rs | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index b322d27..0ea8608 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -2,7 +2,7 @@ pub mod error; pub use error::Error; use gio::{ - prelude::{IOStreamExt, TlsConnectionExt}, + prelude::{CancellableExt, IOStreamExt, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, }; use glib::object::{Cast, IsA}; @@ -50,6 +50,15 @@ impl Connection { // Actions + /// Apply `cancel` action to `Self` [Cancellable](https://docs.gtk.org/gio/method.Cancellable.cancel.html) + /// * return `Error` on `Cancellable` not found + pub fn cancel(&self) -> Result<(), Error> { + match self.cancellable { + Some(ref cancellable) => Ok(cancellable.cancel()), + None => Err(Error::Cancel), + } + } + /// Close owned [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) pub fn close(&self) -> Result<(), Error> { match self.socket_connection.close(self.cancellable.as_ref()) { diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 87131f7..45cdd87 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,6 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { + Cancel, Closed, Rehandshake(glib::Error), SocketConnection(glib::Error), @@ -11,6 +12,7 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::Cancel => write!(f, "Cancellable not found"), Self::Closed => write!(f, "Connection closed"), Self::Rehandshake(e) => { write!(f, "Rehandshake error: {e}") From 559e03f9043f910d2ed27e000270a0895998f48f Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 17:38:03 +0200 Subject: [PATCH 176/392] handle cancellable option --- src/client/connection.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 0ea8608..00278cc 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -60,10 +60,14 @@ impl Connection { } /// Close owned [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - pub fn close(&self) -> Result<(), Error> { - match self.socket_connection.close(self.cancellable.as_ref()) { - Ok(()) => Ok(()), - Err(e) => Err(Error::SocketConnection(e)), + /// * return `Ok(false)` if `Cancellable` not defined + pub fn close(&self) -> Result { + match self.cancellable { + Some(ref cancellable) => match self.socket_connection.close(Some(cancellable)) { + Ok(()) => Ok(true), + Err(e) => Err(Error::SocketConnection(e)), + }, + None => Ok(false), } } From c12c57cc99381c8ea1267245c8dbbbd3f88b577a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:00:38 +0200 Subject: [PATCH 177/392] add connection cancel request --- src/client/session.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/client/session.rs b/src/client/session.rs index aea904a..fb7e6d2 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -68,7 +68,12 @@ impl Session { } } - // Close connection if active yet + // Cancel previous session operations + if let Err(e) = connection.cancel() { + return Err(Error::Connection(e)); + } + + // Close previous session connection if let Err(e) = connection.close() { return Err(Error::Connection(e)); } From 0f8b98c3956d51a61cd24f618bd74b6084856594 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:16:46 +0200 Subject: [PATCH 178/392] update all sessions match certificate scope --- src/client/session.rs | 63 +++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/src/client/session.rs b/src/client/session.rs index fb7e6d2..493688e 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -6,7 +6,7 @@ use gio::{ prelude::{TlsCertificateExt, TlsConnectionExt}, TlsCertificate, }; -use glib::Uri; +use glib::{Uri, UriHideFlags}; use std::{cell::RefCell, collections::HashMap, rc::Rc}; /// Request sessions holder @@ -28,10 +28,6 @@ impl Session { } } - pub fn get(&self, request: &str) -> Option> { - self.index.borrow().get(request).cloned() - } - pub fn set(&self, request: String, connection: Rc) -> Option> { self.index.borrow_mut().insert(request, connection) } @@ -41,41 +37,38 @@ impl Session { /// * force rehandshake on user certificate was changed in runtime (ignore default session resumption by Glib TLS backend implementation) /// * close previous connection match `Uri` if not closed yet pub fn update(&self, uri: &Uri, certificate: Option<&TlsCertificate>) -> Result<(), Error> { - if let Some(connection) = self.get(&uri.to_string()) { - match connection.tls_client_connection { - // User certificate session - Some(ref tls_client_connection) => { - match certificate { - Some(new) => { - // Get previous certificate - if let Some(ref old) = tls_client_connection.certificate() { - // User -> User - if !new.is_same(old) { - rehandshake(connection.as_ref()); + // Get available client connections match `uri` scope + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 + for (request, connection) in self.index.borrow().iter() { + if request.starts_with( + uri.to_string_partial(UriHideFlags::QUERY | UriHideFlags::FRAGMENT) + .as_str(), + ) { + match connection.tls_client_connection { + // User certificate session + Some(ref tls_client_connection) => { + match certificate { + Some(new) => { + // Get previous certificate + if let Some(ref old) = tls_client_connection.certificate() { + // User -> User + if !new.is_same(old) { + rehandshake(connection.as_ref()); + } } } + // User -> Guest + None => rehandshake(connection.as_ref()), + } + } + // Guest + None => { + // Guest -> User + if certificate.is_some() { + rehandshake(connection.as_ref()) } - // User -> Guest - None => rehandshake(connection.as_ref()), } } - // Guest - None => { - // Guest -> User - if certificate.is_some() { - rehandshake(connection.as_ref()) - } - } - } - - // Cancel previous session operations - if let Err(e) = connection.cancel() { - return Err(Error::Connection(e)); - } - - // Close previous session connection - if let Err(e) = connection.close() { - return Err(Error::Connection(e)); } } Ok(()) From c61568a5d82a880b4b4e42437354f632d178c36d Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:19:04 +0200 Subject: [PATCH 179/392] update comments --- src/client/session.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/session.rs b/src/client/session.rs index 493688e..fe28ede 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -32,10 +32,11 @@ impl Session { self.index.borrow_mut().insert(request, connection) } - /// Update existing session for given [Uri](https://docs.gtk.org/glib/struct.Uri.html) + /// Update existing session match [scope](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) + /// for given [Uri](https://docs.gtk.org/glib/struct.Uri.html) /// and [TlsCertificate](https://docs.gtk.org/gio/class.TlsCertificate.html) - /// * force rehandshake on user certificate was changed in runtime (ignore default session resumption by Glib TLS backend implementation) - /// * close previous connection match `Uri` if not closed yet + /// + /// * force rehandshake on user certificate change in runtime (ignore default session resumption by Glib TLS backend) pub fn update(&self, uri: &Uri, certificate: Option<&TlsCertificate>) -> Result<(), Error> { // Get available client connections match `uri` scope // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 @@ -44,6 +45,7 @@ impl Session { uri.to_string_partial(UriHideFlags::QUERY | UriHideFlags::FRAGMENT) .as_str(), ) { + // Begin re-handshake on user certificate change match connection.tls_client_connection { // User certificate session Some(ref tls_client_connection) => { From 5b01a5af54bcc866633b479697e3ba5425681f41 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:20:04 +0200 Subject: [PATCH 180/392] update comment --- src/client/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/session.rs b/src/client/session.rs index fe28ede..df45560 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -73,7 +73,7 @@ impl Session { } } } - Ok(()) + Ok(()) // @TODO result does nothing yet } } From ec34228e0f24531c994ddb5368243a6bf14ffa86 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:24:50 +0200 Subject: [PATCH 181/392] update comment --- src/client.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 942fb17..ef81c2f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -74,11 +74,11 @@ impl Client { /// High-level method make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), /// callback with new `Response`on success or `Error` on failure /// - /// * method does not close new `Connection` created, hold it in `Session`, - /// expects from user manual `Response` handle with close act on complete - /// * if new request match same `uri`, method auto-close previous connection, renew `Session` - /// * method ignores default session resumption provided by Glib TLS backend, - /// implement certificate change ability in application runtime + /// * method does not close new `Connection` by default, hold it in `Session`, + /// expect from user manual `Response` handle with close act on complete + /// * ignore default session resumption provided by Glib TLS backend, + /// instead of that, applies new `certificate` to sessions match `uri` scope + /// * implement certificate change ability in application runtime pub fn request_async( &self, uri: Uri, From 2157e55db9ec103565dae9920070a431d3378d69 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:25:34 +0200 Subject: [PATCH 182/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index ef81c2f..ec321d0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -76,7 +76,7 @@ impl Client { /// /// * method does not close new `Connection` by default, hold it in `Session`, /// expect from user manual `Response` handle with close act on complete - /// * ignore default session resumption provided by Glib TLS backend, + /// * ignores default session resumption provided by Glib TLS backend, /// instead of that, applies new `certificate` to sessions match `uri` scope /// * implement certificate change ability in application runtime pub fn request_async( From 5e9e3aecd2aa9da0204a42dc1ea6e8e4ebcafad4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:27:25 +0200 Subject: [PATCH 183/392] update comment --- src/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index ec321d0..403b74b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -77,7 +77,8 @@ impl Client { /// * method does not close new `Connection` by default, hold it in `Session`, /// expect from user manual `Response` handle with close act on complete /// * ignores default session resumption provided by Glib TLS backend, - /// instead of that, applies new `certificate` to sessions match `uri` scope + /// instead, applies new `certificate` to available sessions match + /// `uri` [scope](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) /// * implement certificate change ability in application runtime pub fn request_async( &self, From 072e6c3e7c5fe677468ed7cb21bf7e7336f8937d Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:33:41 +0200 Subject: [PATCH 184/392] update comments --- src/client/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/session.rs b/src/client/session.rs index df45560..6999773 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -38,14 +38,14 @@ impl Session { /// /// * force rehandshake on user certificate change in runtime (ignore default session resumption by Glib TLS backend) pub fn update(&self, uri: &Uri, certificate: Option<&TlsCertificate>) -> Result<(), Error> { - // Get available client connections match `uri` scope + // Get cached `Client` connections match `uri` scope // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 for (request, connection) in self.index.borrow().iter() { if request.starts_with( uri.to_string_partial(UriHideFlags::QUERY | UriHideFlags::FRAGMENT) .as_str(), ) { - // Begin re-handshake on user certificate change + // Begin re-handshake on `certificate` change match connection.tls_client_connection { // User certificate session Some(ref tls_client_connection) => { From cca3e4daa632b4833b7a0b2e37991b70ae52fdb8 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:52:59 +0200 Subject: [PATCH 185/392] cancel and close previous client sessions --- src/client/session.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/client/session.rs b/src/client/session.rs index 6999773..5e30541 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -72,6 +72,16 @@ impl Session { } } } + + // Cancel previous session operations + if let Err(e) = connection.cancel() { + return Err(Error::Connection(e)); + } + + // Close previous session connection + if let Err(e) = connection.close() { + return Err(Error::Connection(e)); + } } Ok(()) // @TODO result does nothing yet } From efc9b6278627000c3f3cbc29dc0f98cb92f4c4ed Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:59:06 +0200 Subject: [PATCH 186/392] use Cancellable::NONE for re-handshake action --- src/client/connection.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 00278cc..8863f31 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -71,14 +71,11 @@ impl Connection { } } - /// Request force handshake for `Self` + /// Force non-cancellable handshake request for `Self` /// * useful for certificate change in runtime /// * support guest and user sessions pub fn rehandshake(&self) -> Result<(), Error> { - match self - .tls_client_connection()? - .handshake(self.cancellable.as_ref()) - { + match self.tls_client_connection()?.handshake(Cancellable::NONE) { Ok(()) => Ok(()), Err(e) => Err(Error::Rehandshake(e)), } From af4a55659b8b1fa6bfbd6cec174839116a49ac77 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 18:59:57 +0200 Subject: [PATCH 187/392] remove deprecated notice --- src/client/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/session.rs b/src/client/session.rs index 5e30541..da7f667 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -83,7 +83,7 @@ impl Session { return Err(Error::Connection(e)); } } - Ok(()) // @TODO result does nothing yet + Ok(()) } } From fe22b8491611d925a1fea8aa1be0e718462b167a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 19:14:30 +0200 Subject: [PATCH 188/392] update comment --- src/client/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/session.rs b/src/client/session.rs index da7f667..2e918d2 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -78,7 +78,7 @@ impl Session { return Err(Error::Connection(e)); } - // Close previous session connection + // Close previous session connections if let Err(e) = connection.close() { return Err(Error::Connection(e)); } From 911fb13a6993590fd5f2cf2415137b9fab2dee6c Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 30 Nov 2024 19:22:14 +0200 Subject: [PATCH 189/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 403b74b..b01b744 100644 --- a/src/client.rs +++ b/src/client.rs @@ -74,12 +74,12 @@ impl Client { /// High-level method make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), /// callback with new `Response`on success or `Error` on failure /// + /// * implement `certificate` comparison with previously defined for this `uri`, force rehandshake if does not match /// * method does not close new `Connection` by default, hold it in `Session`, /// expect from user manual `Response` handle with close act on complete /// * ignores default session resumption provided by Glib TLS backend, /// instead, applies new `certificate` to available sessions match /// `uri` [scope](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) - /// * implement certificate change ability in application runtime pub fn request_async( &self, uri: Uri, From 3cc9fcd86b5c1ab7721a101fe7cd015ab74b2498 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 02:57:21 +0200 Subject: [PATCH 190/392] replace deprecated re-handshake feature with session-resumption-enabled property set --- src/client.rs | 96 +++++++++++------------------- src/client/connection.rs | 113 +++++++++++------------------------- src/client/error.rs | 4 -- src/client/session.rs | 98 ------------------------------- src/client/session/error.rs | 16 ----- 5 files changed, 68 insertions(+), 259 deletions(-) delete mode 100644 src/client/session.rs delete mode 100644 src/client/session/error.rs diff --git a/src/client.rs b/src/client.rs index b01b744..379b66a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,12 +4,10 @@ pub mod connection; pub mod error; pub mod response; -pub mod session; pub use connection::Connection; pub use error::Error; pub use response::Response; -pub use session::Session; use gio::{ prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsConnectionExt}, @@ -26,7 +24,6 @@ pub const DEFAULT_TIMEOUT: u32 = 10; /// Provides high-level API for session-safe interaction with /// [Gemini](https://geminiprotocol.net) socket server pub struct Client { - session: Rc, pub socket: SocketClient, } @@ -63,23 +60,13 @@ impl Client { }); // Done - Self { - session: Rc::new(Session::new()), - socket, - } + Self { socket } } // Actions /// High-level method make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), /// callback with new `Response`on success or `Error` on failure - /// - /// * implement `certificate` comparison with previously defined for this `uri`, force rehandshake if does not match - /// * method does not close new `Connection` by default, hold it in `Session`, - /// expect from user manual `Response` handle with close act on complete - /// * ignores default session resumption provided by Glib TLS backend, - /// instead, applies new `certificate` to available sessions match - /// `uri` [scope](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) pub fn request_async( &self, uri: Uri, @@ -92,56 +79,43 @@ impl Client { // * guest sessions will not work without! self.socket.set_tls(certificate.is_none()); - // Update previous session if available for this `uri`, does force rehandshake on `certificate` change - match self.session.update(&uri, certificate.as_ref()) { - // Begin new connection - // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid - // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) - Ok(()) => match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { - Ok(network_address) => self.socket.connect_async( - &network_address.clone(), - match cancellable { - Some(ref cancellable) => Some(cancellable.clone()), - None => None::, - } - .as_ref(), - { - let session = self.session.clone(); - move |result| match result { + // Begin new connection + // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid + // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) + match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { + Ok(network_address) => self.socket.connect_async( + &network_address.clone(), + match cancellable { + Some(ref cancellable) => Some(cancellable.clone()), + None => None::, + } + .as_ref(), + move |result| match result { + Ok(connection) => { + // Wrap required connection dependencies into the struct holder + match Connection::new( + connection, + certificate, + Some(network_address), + cancellable.clone(), + ) { Ok(connection) => { - // Wrap required connection dependencies into the struct holder - match Connection::new_wrap( - connection, - certificate, - Some(network_address), - cancellable.clone(), - ) { - Ok(connection) => { - // Wrap to shared reference support clone semantics - let connection = Rc::new(connection); - - // Renew session - session.set(uri.to_string(), connection.clone()); - - // Begin new request - request_async( - connection, - uri.to_string(), - priority, - cancellable, - callback, // result - ) - } - Err(e) => callback(Err(Error::Connection(e))), - } + // Begin new request + request_async( + Rc::new(connection), + uri.to_string(), + priority, + cancellable, + callback, // result + ) } - Err(e) => callback(Err(Error::Connect(e))), + Err(e) => callback(Err(Error::Connection(e))), } - }, - ), - Err(e) => callback(Err(Error::NetworkAddress(e))), - }, - Err(e) => callback(Err(Error::Session(e))), + } + Err(e) => callback(Err(Error::Connect(e))), + }, + ), + Err(e) => callback(Err(Error::NetworkAddress(e))), } } } diff --git a/src/client/connection.rs b/src/client/connection.rs index 8863f31..a2a4013 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -5,20 +5,20 @@ use gio::{ prelude::{CancellableExt, IOStreamExt, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, }; -use glib::object::{Cast, IsA}; +use glib::object::{Cast, IsA, ObjectExt}; pub struct Connection { pub cancellable: Option, - pub server_identity: Option, + pub certificate: Option, pub socket_connection: SocketConnection, - pub tls_client_connection: Option, + pub tls_client_connection: TlsClientConnection, } impl Connection { // Constructors /// Create new `Self` - pub fn new_wrap( + pub fn new( socket_connection: SocketConnection, certificate: Option, server_identity: Option, @@ -30,20 +30,32 @@ impl Connection { Ok(Self { cancellable, - server_identity: server_identity.clone(), + certificate: certificate.clone(), socket_connection: socket_connection.clone(), - tls_client_connection: match certificate { - Some(certificate) => { - match new_tls_client_connection( - &socket_connection, - &certificate, - server_identity.as_ref(), - ) { - Ok(tls_client_connection) => Some(tls_client_connection), - Err(e) => return Err(e), + tls_client_connection: match TlsClientConnection::new( + &socket_connection.clone(), + server_identity.as_ref(), + ) { + Ok(tls_client_connection) => { + // Prevent session resumption (on certificate change in runtime) + tls_client_connection.set_property("session-resumption-enabled", &false); + + // Is user session + // https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates + if let Some(ref certificate) = certificate { + tls_client_connection.set_certificate(certificate); } + + // @TODO handle + // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections + tls_client_connection.set_require_close_notify(true); + + // @TODO validate + // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation + tls_client_connection.connect_accept_certificate(move |_, _, _| true); + tls_client_connection } - None => None, + Err(e) => return Err(Error::TlsClientConnection(e)), }, }) } @@ -71,77 +83,18 @@ impl Connection { } } - /// Force non-cancellable handshake request for `Self` - /// * useful for certificate change in runtime - /// * support guest and user sessions - pub fn rehandshake(&self) -> Result<(), Error> { - match self.tls_client_connection()?.handshake(Cancellable::NONE) { - Ok(()) => Ok(()), - Err(e) => Err(Error::Rehandshake(e)), - } - } - // Getters - /// Upcast [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + /// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// or [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) (if available) - /// * wanted to keep `Connection` active in async I/O context + /// * useful also to keep `Connection` active in async I/O context pub fn stream(&self) -> impl IsA { - match self.tls_client_connection.clone() { - Some(tls_client_connection) => tls_client_connection.upcast::(), + // * do not replace with `tls_client_connection.base_io_stream()` + // as it will not work for user certificate sessions! + match self.certificate { + Some(_) => self.tls_client_connection.clone().upcast::(), None => self.socket_connection.clone().upcast::(), } } - - /// Get [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) for `Self` - /// * compatible with both user and guest connection types - pub fn tls_client_connection(&self) -> Result { - match self.tls_client_connection.clone() { - // User session - Some(tls_client_connection) => Ok(tls_client_connection), - // Guest session - None => { - // Create new wrapper for `IOStream` to interact `TlsClientConnection` API - match TlsClientConnection::new( - self.stream().as_ref(), - self.server_identity.as_ref(), - ) { - Ok(tls_client_connection) => Ok(tls_client_connection), - Err(e) => Err(Error::TlsClientConnection(e)), - } - } - } - } -} - -// Tools - -pub fn new_tls_client_connection( - socket_connection: &SocketConnection, - certificate: &TlsCertificate, - server_identity: Option<&NetworkAddress>, -) -> Result { - if socket_connection.is_closed() { - return Err(Error::Closed); - } - - // https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls - match TlsClientConnection::new(socket_connection, server_identity) { - Ok(tls_client_connection) => { - // https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates - tls_client_connection.set_certificate(certificate); - - // @TODO handle exceptions - // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections - tls_client_connection.set_require_close_notify(true); - - // @TODO host validation - // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation - tls_client_connection.connect_accept_certificate(move |_, _, _| true); - - Ok(tls_client_connection) - } - Err(e) => Err(Error::TlsClientConnection(e)), - } } diff --git a/src/client/error.rs b/src/client/error.rs index bbb159a..944ba90 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -9,7 +9,6 @@ pub enum Error { OutputStream(glib::Error), Request(glib::Error), Response(crate::client::response::Error), - Session(crate::client::session::Error), } impl Display for Error { @@ -36,9 +35,6 @@ impl Display for Error { Self::Response(e) => { write!(f, "Response error: {e}") } - Self::Session(e) => { - write!(f, "Session error: {e}") - } } } } diff --git a/src/client/session.rs b/src/client/session.rs deleted file mode 100644 index 2e918d2..0000000 --- a/src/client/session.rs +++ /dev/null @@ -1,98 +0,0 @@ -mod error; -pub use error::Error; - -use super::Connection; -use gio::{ - prelude::{TlsCertificateExt, TlsConnectionExt}, - TlsCertificate, -}; -use glib::{Uri, UriHideFlags}; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; - -/// Request sessions holder -/// * useful to keep connections open in async context and / or validate TLS certificate updates in runtime -pub struct Session { - index: RefCell>>, -} - -impl Default for Session { - fn default() -> Self { - Self::new() - } -} - -impl Session { - pub fn new() -> Self { - Self { - index: RefCell::new(HashMap::new()), - } - } - - pub fn set(&self, request: String, connection: Rc) -> Option> { - self.index.borrow_mut().insert(request, connection) - } - - /// Update existing session match [scope](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) - /// for given [Uri](https://docs.gtk.org/glib/struct.Uri.html) - /// and [TlsCertificate](https://docs.gtk.org/gio/class.TlsCertificate.html) - /// - /// * force rehandshake on user certificate change in runtime (ignore default session resumption by Glib TLS backend) - pub fn update(&self, uri: &Uri, certificate: Option<&TlsCertificate>) -> Result<(), Error> { - // Get cached `Client` connections match `uri` scope - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - for (request, connection) in self.index.borrow().iter() { - if request.starts_with( - uri.to_string_partial(UriHideFlags::QUERY | UriHideFlags::FRAGMENT) - .as_str(), - ) { - // Begin re-handshake on `certificate` change - match connection.tls_client_connection { - // User certificate session - Some(ref tls_client_connection) => { - match certificate { - Some(new) => { - // Get previous certificate - if let Some(ref old) = tls_client_connection.certificate() { - // User -> User - if !new.is_same(old) { - rehandshake(connection.as_ref()); - } - } - } - // User -> Guest - None => rehandshake(connection.as_ref()), - } - } - // Guest - None => { - // Guest -> User - if certificate.is_some() { - rehandshake(connection.as_ref()) - } - } - } - } - - // Cancel previous session operations - if let Err(e) = connection.cancel() { - return Err(Error::Connection(e)); - } - - // Close previous session connections - if let Err(e) = connection.close() { - return Err(Error::Connection(e)); - } - } - Ok(()) - } -} - -// Tools - -/// Applies re-handshake to `Connection` -/// to prevent default session resumption on user certificate change in runtime -pub fn rehandshake(connection: &Connection) { - if let Err(e) = connection.rehandshake() { - println!("warning: {e}"); // @TODO keep in mind until solution for TLS 1.3 - } -} diff --git a/src/client/session/error.rs b/src/client/session/error.rs deleted file mode 100644 index a974d1a..0000000 --- a/src/client/session/error.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Connection(crate::client::connection::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::Connection(e) => { - write!(f, "Connection error: {e}") - } - } - } -} From 273dac139e165385cd00d05e9926b103b80ab7ee Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 03:04:01 +0200 Subject: [PATCH 191/392] remove extra certificate holder --- src/client/connection.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index a2a4013..3a8b17c 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -9,7 +9,6 @@ use glib::object::{Cast, IsA, ObjectExt}; pub struct Connection { pub cancellable: Option, - pub certificate: Option, pub socket_connection: SocketConnection, pub tls_client_connection: TlsClientConnection, } @@ -30,7 +29,6 @@ impl Connection { Ok(Self { cancellable, - certificate: certificate.clone(), socket_connection: socket_connection.clone(), tls_client_connection: match TlsClientConnection::new( &socket_connection.clone(), @@ -91,10 +89,10 @@ impl Connection { /// * useful also to keep `Connection` active in async I/O context pub fn stream(&self) -> impl IsA { // * do not replace with `tls_client_connection.base_io_stream()` - // as it will not work for user certificate sessions! - match self.certificate { - Some(_) => self.tls_client_connection.clone().upcast::(), - None => self.socket_connection.clone().upcast::(), + // as it will not work properly for user certificate sessions! + match self.tls_client_connection.certificate().is_some() { + true => self.tls_client_connection.clone().upcast::(), + false => self.socket_connection.clone().upcast::(), } } } From 17b2fcaaae67e43ee619506014e17a2bfd76d15e Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 03:05:46 +0200 Subject: [PATCH 192/392] add comments --- src/client/connection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 3a8b17c..4768642 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -91,8 +91,8 @@ impl Connection { // * do not replace with `tls_client_connection.base_io_stream()` // as it will not work properly for user certificate sessions! match self.tls_client_connection.certificate().is_some() { - true => self.tls_client_connection.clone().upcast::(), - false => self.socket_connection.clone().upcast::(), + true => self.tls_client_connection.clone().upcast::(), // is user session + false => self.socket_connection.clone().upcast::(), // is guest session } } } From 98c6150f749e03fdbbab122a284cfe2ef5d49e1c Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 03:11:40 +0200 Subject: [PATCH 193/392] update comments --- src/client.rs | 2 ++ src/client/connection.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/client.rs b/src/client.rs index 379b66a..2ebc425 100644 --- a/src/client.rs +++ b/src/client.rs @@ -67,6 +67,8 @@ impl Client { /// High-level method make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), /// callback with new `Response`on success or `Error` on failure + /// * compatible with user (certificate) and guest (certificate-less) connection types + /// * disables default `session-resumption-enabled` property to apply certificate change ability in runtime pub fn request_async( &self, uri: Uri, diff --git a/src/client/connection.rs b/src/client/connection.rs index 4768642..2fe369d 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -86,6 +86,7 @@ impl Connection { /// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// or [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) (if available) + /// * compatible with user (certificate) and guest (certificate-less) connection types /// * useful also to keep `Connection` active in async I/O context pub fn stream(&self) -> impl IsA { // * do not replace with `tls_client_connection.base_io_stream()` From 0cc6d10017aea604bfdc89e9ab4dfae7a99a18f7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 03:21:43 +0200 Subject: [PATCH 194/392] remove rc wrap --- src/client.rs | 5 ++--- src/client/response.rs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2ebc425..ad49409 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,7 +15,6 @@ use gio::{ TlsClientConnection, }; use glib::{object::Cast, Bytes, Priority, Uri}; -use std::rc::Rc; pub const DEFAULT_TIMEOUT: u32 = 10; @@ -104,7 +103,7 @@ impl Client { Ok(connection) => { // Begin new request request_async( - Rc::new(connection), + connection, uri.to_string(), priority, cancellable, @@ -125,7 +124,7 @@ impl Client { /// Middle-level method to make new request to `Connection` /// * callback with new `Response`on success or `Error` on failure pub fn request_async( - connection: Rc, + connection: Connection, query: String, priority: Option, cancellable: Option, diff --git a/src/client/response.rs b/src/client/response.rs index 345b262..b4ebe87 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -10,10 +10,9 @@ pub use meta::Meta; use super::Connection; use gio::Cancellable; use glib::Priority; -use std::rc::Rc; pub struct Response { - pub connection: Rc, + pub connection: Connection, pub meta: Meta, } @@ -21,7 +20,7 @@ impl Response { // Constructors pub fn from_request_async( - connection: Rc, + connection: Connection, priority: Option, cancellable: Option, callback: impl FnOnce(Result) + 'static, From 403817273591e451a5e763bd0d87b98fd687b7ec Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 03:47:20 +0200 Subject: [PATCH 195/392] rename variable to socket_connection --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index ad49409..cb10c0f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -92,10 +92,10 @@ impl Client { } .as_ref(), move |result| match result { - Ok(connection) => { + Ok(socket_connection) => { // Wrap required connection dependencies into the struct holder match Connection::new( - connection, + socket_connection, certificate, Some(network_address), cancellable.clone(), From 99583aa719734c7ba98c992500b64fde25cad91d Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 03:58:13 +0200 Subject: [PATCH 196/392] remove extra reference --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 2fe369d..12612ab 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -36,7 +36,7 @@ impl Connection { ) { Ok(tls_client_connection) => { // Prevent session resumption (on certificate change in runtime) - tls_client_connection.set_property("session-resumption-enabled", &false); + tls_client_connection.set_property("session-resumption-enabled", false); // Is user session // https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates From 2df9f36599b16efdcba57f73510e49aae4e8b91f Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 03:59:36 +0200 Subject: [PATCH 197/392] fix cancellation construction --- src/client/connection.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 12612ab..ee2d4e1 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -64,7 +64,10 @@ impl Connection { /// * return `Error` on `Cancellable` not found pub fn cancel(&self) -> Result<(), Error> { match self.cancellable { - Some(ref cancellable) => Ok(cancellable.cancel()), + Some(ref cancellable) => { + cancellable.cancel(); + Ok(()) + } None => Err(Error::Cancel), } } From 8f910672e209e89ea14e8290d8c89b44662b0606 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 04:35:19 +0200 Subject: [PATCH 198/392] require Priority, Cancellable arguments, remove extra members --- src/client.rs | 33 +++++++------------------ src/client/connection.rs | 41 ++++---------------------------- src/client/connection/error.rs | 12 ---------- src/client/response.rs | 4 ++-- src/client/response/data/text.rs | 18 +++++--------- src/client/response/meta.rs | 18 +++++--------- src/gio/memory_input_stream.rs | 6 ++--- 7 files changed, 30 insertions(+), 102 deletions(-) diff --git a/src/client.rs b/src/client.rs index cb10c0f..0638661 100644 --- a/src/client.rs +++ b/src/client.rs @@ -71,8 +71,8 @@ impl Client { pub fn request_async( &self, uri: Uri, - priority: Option, - cancellable: Option, + priority: Priority, + cancellable: Cancellable, certificate: Option, callback: impl Fn(Result) + 'static, ) { @@ -86,20 +86,12 @@ impl Client { match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { Ok(network_address) => self.socket.connect_async( &network_address.clone(), - match cancellable { - Some(ref cancellable) => Some(cancellable.clone()), - None => None::, - } - .as_ref(), + Some(&cancellable.clone()), move |result| match result { Ok(socket_connection) => { // Wrap required connection dependencies into the struct holder - match Connection::new( - socket_connection, - certificate, - Some(network_address), - cancellable.clone(), - ) { + match Connection::new(socket_connection, certificate, Some(network_address)) + { Ok(connection) => { // Begin new request request_async( @@ -126,21 +118,14 @@ impl Client { pub fn request_async( connection: Connection, query: String, - priority: Option, - cancellable: Option, + priority: Priority, + cancellable: Cancellable, callback: impl Fn(Result) + 'static, ) { connection.stream().output_stream().write_bytes_async( &Bytes::from(format!("{query}\r\n").as_bytes()), - match priority { - Some(priority) => priority, - None => Priority::DEFAULT, - }, - match cancellable { - Some(ref cancellable) => Some(cancellable.clone()), - None => None::, - } - .as_ref(), + priority, + Some(&cancellable.clone()), move |result| match result { Ok(_) => { Response::from_request_async(connection, priority, cancellable, move |result| { diff --git a/src/client/connection.rs b/src/client/connection.rs index ee2d4e1..004e5ff 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -2,13 +2,12 @@ pub mod error; pub use error::Error; use gio::{ - prelude::{CancellableExt, IOStreamExt, TlsConnectionExt}, - Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, + prelude::TlsConnectionExt, IOStream, NetworkAddress, SocketConnection, TlsCertificate, + TlsClientConnection, }; use glib::object::{Cast, IsA, ObjectExt}; pub struct Connection { - pub cancellable: Option, pub socket_connection: SocketConnection, pub tls_client_connection: TlsClientConnection, } @@ -21,17 +20,11 @@ impl Connection { socket_connection: SocketConnection, certificate: Option, server_identity: Option, - cancellable: Option, ) -> Result { - if socket_connection.is_closed() { - return Err(Error::Closed); - } - Ok(Self { - cancellable, socket_connection: socket_connection.clone(), tls_client_connection: match TlsClientConnection::new( - &socket_connection.clone(), + &socket_connection, server_identity.as_ref(), ) { Ok(tls_client_connection) => { @@ -58,38 +51,12 @@ impl Connection { }) } - // Actions - - /// Apply `cancel` action to `Self` [Cancellable](https://docs.gtk.org/gio/method.Cancellable.cancel.html) - /// * return `Error` on `Cancellable` not found - pub fn cancel(&self) -> Result<(), Error> { - match self.cancellable { - Some(ref cancellable) => { - cancellable.cancel(); - Ok(()) - } - None => Err(Error::Cancel), - } - } - - /// Close owned [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - /// * return `Ok(false)` if `Cancellable` not defined - pub fn close(&self) -> Result { - match self.cancellable { - Some(ref cancellable) => match self.socket_connection.close(Some(cancellable)) { - Ok(()) => Ok(true), - Err(e) => Err(Error::SocketConnection(e)), - }, - None => Ok(false), - } - } - // Getters /// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// or [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) (if available) - /// * compatible with user (certificate) and guest (certificate-less) connection types + /// * compatible with user (certificate) and guest (certificate-less) connection type /// * useful also to keep `Connection` active in async I/O context pub fn stream(&self) -> impl IsA { // * do not replace with `tls_client_connection.base_io_stream()` diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 45cdd87..ab6ff8e 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,24 +2,12 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Cancel, - Closed, - Rehandshake(glib::Error), - SocketConnection(glib::Error), TlsClientConnection(glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Cancel => write!(f, "Cancellable not found"), - Self::Closed => write!(f, "Connection closed"), - Self::Rehandshake(e) => { - write!(f, "Rehandshake error: {e}") - } - Self::SocketConnection(e) => { - write!(f, "Socket connection error: {e}") - } Self::TlsClientConnection(e) => { write!(f, "TLS client connection error: {e}") } diff --git a/src/client/response.rs b/src/client/response.rs index b4ebe87..be2f7d8 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -21,8 +21,8 @@ impl Response { pub fn from_request_async( connection: Connection, - priority: Option, - cancellable: Option, + priority: Priority, + cancellable: Cancellable, callback: impl FnOnce(Result) + 'static, ) { Meta::from_stream_async(connection.stream(), priority, cancellable, |result| { diff --git a/src/client/response/data/text.rs b/src/client/response/data/text.rs index 4de4654..2d3cf2c 100644 --- a/src/client/response/data/text.rs +++ b/src/client/response/data/text.rs @@ -51,21 +51,15 @@ impl Text { /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) pub fn from_stream_async( stream: impl IsA, - priority: Option, - cancellable: Option, + priority: Priority, + cancellable: Cancellable, on_complete: impl FnOnce(Result) + 'static, ) { read_all_from_stream_async( Vec::with_capacity(BUFFER_CAPACITY), stream, - match cancellable { - Some(value) => Some(value), - None => None::, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, + cancellable, + priority, |result| match result { Ok(buffer) => on_complete(Self::from_utf8(&buffer)), Err(e) => on_complete(Err(e)), @@ -83,14 +77,14 @@ impl Text { pub fn read_all_from_stream_async( mut buffer: Vec, stream: impl IsA, - cancelable: Option, + cancelable: Cancellable, priority: Priority, callback: impl FnOnce(Result, Error>) + 'static, ) { stream.input_stream().read_bytes_async( BUFFER_CAPACITY, priority, - cancelable.clone().as_ref(), + Some(&cancelable.clone()), move |result| match result { Ok(bytes) => { // No bytes were read, end of stream diff --git a/src/client/response/meta.rs b/src/client/response/meta.rs index 42bd4dd..26388ad 100644 --- a/src/client/response/meta.rs +++ b/src/client/response/meta.rs @@ -78,21 +78,15 @@ impl Meta { /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) pub fn from_stream_async( stream: impl IsA, - priority: Option, - cancellable: Option, + priority: Priority, + cancellable: Cancellable, on_complete: impl FnOnce(Result) + 'static, ) { read_from_stream_async( Vec::with_capacity(MAX_LEN), stream, - match cancellable { - Some(value) => Some(value), - None => None::, - }, - match priority { - Some(value) => value, - None => Priority::DEFAULT, - }, + cancellable, + priority, |result| match result { Ok(buffer) => on_complete(Self::from_utf8(&buffer)), Err(e) => on_complete(Err(e)), @@ -110,14 +104,14 @@ impl Meta { pub fn read_from_stream_async( mut buffer: Vec, stream: impl IsA, - cancellable: Option, + cancellable: Cancellable, priority: Priority, on_complete: impl FnOnce(Result, Error>) + 'static, ) { stream.input_stream().read_async( vec![0], priority, - cancellable.clone().as_ref(), + Some(&cancellable.clone()), move |result| match result { Ok((mut bytes, size)) => { // Expect valid header length diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index fc175e0..dbb0bc3 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -15,7 +15,7 @@ use glib::{object::IsA, Bytes, Priority}; /// * calculate bytes processed on chunk load pub fn from_stream_async( base_io_stream: impl IsA, - cancelable: Option, + cancelable: Cancellable, priority: Priority, bytes_in_chunk: usize, bytes_total_limit: usize, @@ -38,7 +38,7 @@ pub fn from_stream_async( pub fn read_all_from_stream_async( memory_input_stream: MemoryInputStream, base_io_stream: impl IsA, - cancelable: Option, + cancelable: Cancellable, priority: Priority, bytes: (usize, usize, usize), callback: ( @@ -52,7 +52,7 @@ pub fn read_all_from_stream_async( base_io_stream.input_stream().read_bytes_async( bytes_in_chunk, priority, - cancelable.clone().as_ref(), + Some(&cancelable.clone()), move |result| match result { Ok(bytes) => { // Update bytes total From 6330bbfd8560f005e12c3011b33f222926f252b8 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 04:49:10 +0200 Subject: [PATCH 199/392] rename method, update comments --- src/client.rs | 6 +++--- src/client/response.rs | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0638661..731f91b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -113,8 +113,8 @@ impl Client { } } -/// Middle-level method to make new request to `Connection` -/// * callback with new `Response`on success or `Error` on failure +/// Middle-level helper, makes new request to available `Connection` +/// * callback with new `Response` on success or `Error` on failure pub fn request_async( connection: Connection, query: String, @@ -128,7 +128,7 @@ pub fn request_async( Some(&cancellable.clone()), move |result| match result { Ok(_) => { - Response::from_request_async(connection, priority, cancellable, move |result| { + Response::from_connection_async(connection, priority, cancellable, move |result| { callback(match result { Ok(response) => Ok(response), Err(e) => Err(Error::Response(e)), diff --git a/src/client/response.rs b/src/client/response.rs index be2f7d8..3ede7d0 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -19,7 +19,9 @@ pub struct Response { impl Response { // Constructors - pub fn from_request_async( + /// 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, From 7d90d974a0d488c9cc9597324729acce97fa3e3a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 06:51:41 +0200 Subject: [PATCH 200/392] drop extra move --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 004e5ff..802a33c 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -43,7 +43,7 @@ impl Connection { // @TODO validate // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation - tls_client_connection.connect_accept_certificate(move |_, _, _| true); + tls_client_connection.connect_accept_certificate(|_, _, _| true); tls_client_connection } Err(e) => return Err(Error::TlsClientConnection(e)), From 730af453f657493b1eae6ed2fa7b446c190c0725 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 07:04:21 +0200 Subject: [PATCH 201/392] remove extra clone --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 802a33c..8960f4a 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -22,7 +22,6 @@ impl Connection { server_identity: Option, ) -> Result { Ok(Self { - socket_connection: socket_connection.clone(), tls_client_connection: match TlsClientConnection::new( &socket_connection, server_identity.as_ref(), @@ -48,6 +47,7 @@ impl Connection { } Err(e) => return Err(Error::TlsClientConnection(e)), }, + socket_connection, }) } From 70fc128c291e529d0e3954d395c3755554e7e5fd Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 08:10:12 +0200 Subject: [PATCH 202/392] drop extra move --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 731f91b..bb6ef86 100644 --- a/src/client.rs +++ b/src/client.rs @@ -45,7 +45,7 @@ impl Client { socket.set_timeout(DEFAULT_TIMEOUT); // Connect events - socket.connect_event(move |_, event, _, stream| { + socket.connect_event(|_, event, _, stream| { // This condition have effect only for guest TLS connections // * for user certificates validation, use `Connection` impl if event == SocketClientEvent::TlsHandshaking { From 4767929050cd1e81441d6e50be76d8dd4cd557af Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 08:50:28 +0200 Subject: [PATCH 203/392] update response namespace --- src/client.rs | 43 ++++------------- src/client/connection.rs | 46 +++++++++++++++++-- src/client/connection/error.rs | 8 ++++ src/client/{ => connection}/response.rs | 0 src/client/{ => connection}/response/data.rs | 0 .../{ => connection}/response/data/text.rs | 0 .../response/data/text/error.rs | 0 src/client/{ => connection}/response/error.rs | 0 src/client/{ => connection}/response/meta.rs | 0 .../{ => connection}/response/meta/charset.rs | 0 .../{ => connection}/response/meta/data.rs | 0 .../response/meta/data/error.rs | 0 .../{ => connection}/response/meta/error.rs | 0 .../response/meta/language.rs | 0 .../{ => connection}/response/meta/mime.rs | 0 .../response/meta/mime/error.rs | 0 .../{ => connection}/response/meta/status.rs | 0 .../response/meta/status/error.rs | 0 src/client/error.rs | 12 ----- 19 files changed, 59 insertions(+), 50 deletions(-) rename src/client/{ => connection}/response.rs (100%) rename src/client/{ => connection}/response/data.rs (100%) rename src/client/{ => connection}/response/data/text.rs (100%) rename src/client/{ => connection}/response/data/text/error.rs (100%) rename src/client/{ => connection}/response/error.rs (100%) rename src/client/{ => connection}/response/meta.rs (100%) rename src/client/{ => connection}/response/meta/charset.rs (100%) rename src/client/{ => connection}/response/meta/data.rs (100%) rename src/client/{ => connection}/response/meta/data/error.rs (100%) rename src/client/{ => connection}/response/meta/error.rs (100%) rename src/client/{ => connection}/response/meta/language.rs (100%) rename src/client/{ => connection}/response/meta/mime.rs (100%) rename src/client/{ => connection}/response/meta/mime/error.rs (100%) rename src/client/{ => connection}/response/meta/status.rs (100%) rename src/client/{ => connection}/response/meta/status/error.rs (100%) diff --git a/src/client.rs b/src/client.rs index bb6ef86..5acd3c0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,18 +3,16 @@ pub mod connection; pub mod error; -pub mod response; pub use connection::Connection; pub use error::Error; -pub use response::Response; use gio::{ - prelude::{IOStreamExt, OutputStreamExt, SocketClientExt, TlsConnectionExt}, + prelude::{SocketClientExt, TlsConnectionExt}, Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificate, TlsClientConnection, }; -use glib::{object::Cast, Bytes, Priority, Uri}; +use glib::{object::Cast, Priority, Uri}; pub const DEFAULT_TIMEOUT: u32 = 10; @@ -74,7 +72,7 @@ impl Client { priority: Priority, cancellable: Cancellable, certificate: Option, - callback: impl Fn(Result) + 'static, + callback: impl Fn(Result) + 'static, ) { // Toggle socket mode // * guest sessions will not work without! @@ -94,12 +92,14 @@ impl Client { { Ok(connection) => { // Begin new request - request_async( - connection, + connection.request_async( uri.to_string(), priority, cancellable, - callback, // result + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), + }, ) } Err(e) => callback(Err(Error::Connection(e))), @@ -112,30 +112,3 @@ impl Client { } } } - -/// Middle-level helper, makes new request to available `Connection` -/// * callback with new `Response` on success or `Error` on failure -pub fn request_async( - connection: Connection, - query: String, - priority: Priority, - cancellable: Cancellable, - callback: impl Fn(Result) + 'static, -) { - connection.stream().output_stream().write_bytes_async( - &Bytes::from(format!("{query}\r\n").as_bytes()), - priority, - Some(&cancellable.clone()), - move |result| match result { - Ok(_) => { - Response::from_connection_async(connection, priority, cancellable, move |result| { - callback(match result { - Ok(response) => Ok(response), - Err(e) => Err(Error::Response(e)), - }) - }) - } - Err(e) => callback(Err(Error::OutputStream(e))), - }, - ); -} diff --git a/src/client/connection.rs b/src/client/connection.rs index 8960f4a..e5eb37c 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,11 +1,17 @@ pub mod error; +pub mod response; + pub use error::Error; +pub use response::Response; use gio::{ - prelude::TlsConnectionExt, IOStream, NetworkAddress, SocketConnection, TlsCertificate, - TlsClientConnection, + prelude::{IOStreamExt, OutputStreamExt, TlsConnectionExt}, + Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, +}; +use glib::{ + object::{Cast, IsA, ObjectExt}, + Bytes, Priority, }; -use glib::object::{Cast, IsA, ObjectExt}; pub struct Connection { pub socket_connection: SocketConnection, @@ -51,6 +57,40 @@ impl Connection { }) } + // Actions + + /// Make new request to available `Connection` + /// * callback with new `Response` on success or `Error` on failure + pub fn request_async( + self, + query: String, + priority: Priority, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, + ) { + self.tls_client_connection + .output_stream() + .write_bytes_async( + &Bytes::from(format!("{query}\r\n").as_bytes()), + priority, + Some(&cancellable.clone()), + move |result| match result { + Ok(_) => Response::from_connection_async( + self, + priority, + cancellable, + move |result| { + callback(match result { + Ok(response) => Ok(response), + Err(e) => Err(Error::Response(e)), + }) + }, + ), + Err(e) => callback(Err(Error::Stream(e))), + }, + ); + } + // Getters /// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html) diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index ab6ff8e..2948f0f 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -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}") } diff --git a/src/client/response.rs b/src/client/connection/response.rs similarity index 100% rename from src/client/response.rs rename to src/client/connection/response.rs diff --git a/src/client/response/data.rs b/src/client/connection/response/data.rs similarity index 100% rename from src/client/response/data.rs rename to src/client/connection/response/data.rs diff --git a/src/client/response/data/text.rs b/src/client/connection/response/data/text.rs similarity index 100% rename from src/client/response/data/text.rs rename to src/client/connection/response/data/text.rs diff --git a/src/client/response/data/text/error.rs b/src/client/connection/response/data/text/error.rs similarity index 100% rename from src/client/response/data/text/error.rs rename to src/client/connection/response/data/text/error.rs diff --git a/src/client/response/error.rs b/src/client/connection/response/error.rs similarity index 100% rename from src/client/response/error.rs rename to src/client/connection/response/error.rs diff --git a/src/client/response/meta.rs b/src/client/connection/response/meta.rs similarity index 100% rename from src/client/response/meta.rs rename to src/client/connection/response/meta.rs diff --git a/src/client/response/meta/charset.rs b/src/client/connection/response/meta/charset.rs similarity index 100% rename from src/client/response/meta/charset.rs rename to src/client/connection/response/meta/charset.rs diff --git a/src/client/response/meta/data.rs b/src/client/connection/response/meta/data.rs similarity index 100% rename from src/client/response/meta/data.rs rename to src/client/connection/response/meta/data.rs diff --git a/src/client/response/meta/data/error.rs b/src/client/connection/response/meta/data/error.rs similarity index 100% rename from src/client/response/meta/data/error.rs rename to src/client/connection/response/meta/data/error.rs diff --git a/src/client/response/meta/error.rs b/src/client/connection/response/meta/error.rs similarity index 100% rename from src/client/response/meta/error.rs rename to src/client/connection/response/meta/error.rs diff --git a/src/client/response/meta/language.rs b/src/client/connection/response/meta/language.rs similarity index 100% rename from src/client/response/meta/language.rs rename to src/client/connection/response/meta/language.rs diff --git a/src/client/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs similarity index 100% rename from src/client/response/meta/mime.rs rename to src/client/connection/response/meta/mime.rs diff --git a/src/client/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs similarity index 100% rename from src/client/response/meta/mime/error.rs rename to src/client/connection/response/meta/mime/error.rs diff --git a/src/client/response/meta/status.rs b/src/client/connection/response/meta/status.rs similarity index 100% rename from src/client/response/meta/status.rs rename to src/client/connection/response/meta/status.rs diff --git a/src/client/response/meta/status/error.rs b/src/client/connection/response/meta/status/error.rs similarity index 100% rename from src/client/response/meta/status/error.rs rename to src/client/connection/response/meta/status/error.rs diff --git a/src/client/error.rs b/src/client/error.rs index 944ba90..0ebb3c3 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -3,20 +3,14 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { Connect(glib::Error), - Connectable(String), Connection(crate::client::connection::Error), NetworkAddress(crate::gio::network_address::Error), - OutputStream(glib::Error), Request(glib::Error), - Response(crate::client::response::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Connectable(uri) => { - write!(f, "Could not create connectable address for {uri}") - } Self::Connection(e) => { write!(f, "Connection error: {e}") } @@ -26,15 +20,9 @@ impl Display for Error { Self::NetworkAddress(e) => { write!(f, "Network address error: {e}") } - Self::OutputStream(e) => { - write!(f, "Output stream error: {e}") - } Self::Request(e) => { write!(f, "Request error: {e}") } - Self::Response(e) => { - write!(f, "Response error: {e}") - } } } } From 8b6f2200f5be91c90ef63eab0dfd1d957322db3c Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 08:51:59 +0200 Subject: [PATCH 204/392] update comment --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index e5eb37c..b606438 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -59,7 +59,7 @@ impl Connection { // Actions - /// Make new request to available `Connection` + /// Make new request to `Self` connection /// * callback with new `Response` on success or `Error` on failure pub fn request_async( self, From e442a2880a96eb7d8758eea8e97a383fae1b2c50 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 09:15:53 +0200 Subject: [PATCH 205/392] fix guest certificate session cast --- src/client/connection.rs | 102 +++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index b606438..1c9fc27 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -15,7 +15,7 @@ use glib::{ pub struct Connection { pub socket_connection: SocketConnection, - pub tls_client_connection: TlsClientConnection, + pub tls_client_connection: Option, } impl Connection { @@ -28,30 +28,17 @@ impl Connection { server_identity: Option, ) -> Result { Ok(Self { - tls_client_connection: match TlsClientConnection::new( - &socket_connection, - server_identity.as_ref(), - ) { - Ok(tls_client_connection) => { - // Prevent session resumption (on certificate change in runtime) - tls_client_connection.set_property("session-resumption-enabled", false); - - // Is user session - // https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates - if let Some(ref certificate) = certificate { - tls_client_connection.set_certificate(certificate); + tls_client_connection: match certificate { + Some(ref certificate) => { + match new_tls_client_connection(&socket_connection, server_identity.as_ref()) { + Ok(tls_client_connection) => { + tls_client_connection.set_certificate(certificate); + Some(tls_client_connection) + } + Err(e) => return Err(e), } - - // @TODO handle - // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections - tls_client_connection.set_require_close_notify(true); - - // @TODO validate - // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation - tls_client_connection.connect_accept_certificate(|_, _, _| true); - tls_client_connection } - Err(e) => return Err(Error::TlsClientConnection(e)), + None => None, }, socket_connection, }) @@ -68,27 +55,22 @@ impl Connection { cancellable: Cancellable, callback: impl Fn(Result) + 'static, ) { - self.tls_client_connection - .output_stream() - .write_bytes_async( - &Bytes::from(format!("{query}\r\n").as_bytes()), - priority, - Some(&cancellable.clone()), - move |result| match result { - Ok(_) => Response::from_connection_async( - self, - priority, - cancellable, - move |result| { - callback(match result { - Ok(response) => Ok(response), - Err(e) => Err(Error::Response(e)), - }) - }, - ), - Err(e) => callback(Err(Error::Stream(e))), - }, - ); + self.stream().output_stream().write_bytes_async( + &Bytes::from(format!("{query}\r\n").as_bytes()), + priority, + Some(&cancellable.clone()), + move |result| match result { + Ok(_) => { + Response::from_connection_async(self, priority, cancellable, move |result| { + callback(match result { + Ok(response) => Ok(response), + Err(e) => Err(Error::Response(e)), + }) + }) + } + Err(e) => callback(Err(Error::Stream(e))), + }, + ); } // Getters @@ -101,9 +83,35 @@ impl Connection { pub fn stream(&self) -> impl IsA { // * do not replace with `tls_client_connection.base_io_stream()` // as it will not work properly for user certificate sessions! - match self.tls_client_connection.certificate().is_some() { - true => self.tls_client_connection.clone().upcast::(), // is user session - false => self.socket_connection.clone().upcast::(), // is guest session + match self.tls_client_connection.is_some() { + true => self + .tls_client_connection + .clone() + .unwrap() + .upcast::(), // is user session + false => self.socket_connection.clone().upcast::(), // is guest session } } } + +fn new_tls_client_connection( + socket_connection: &SocketConnection, + server_identity: Option<&NetworkAddress>, +) -> Result { + match TlsClientConnection::new(socket_connection, server_identity) { + Ok(tls_client_connection) => { + // Prevent session resumption (on certificate change in runtime) + tls_client_connection.set_property("session-resumption-enabled", false); + + // @TODO handle + // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections + tls_client_connection.set_require_close_notify(true); + + // @TODO validate + // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation + tls_client_connection.connect_accept_certificate(|_, _, _| true); + Ok(tls_client_connection) + } + Err(e) => Err(Error::TlsClientConnection(e)), + } +} From 8947052718455c7aeae18765c914f2e8cb3a0480 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 09:23:23 +0200 Subject: [PATCH 206/392] make new_tls_client_connection public, update comments --- src/client/connection.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 1c9fc27..2f6b128 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -15,7 +15,7 @@ use glib::{ pub struct Connection { pub socket_connection: SocketConnection, - pub tls_client_connection: Option, + pub tls_client_connection: Option, // is user certificate session } impl Connection { @@ -75,26 +75,30 @@ impl Connection { // Getters - /// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + /// Cast [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// or [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) (if available) /// * compatible with user (certificate) and guest (certificate-less) connection type - /// * useful also to keep `Connection` active in async I/O context + /// * useful to keep `Connection` reference active in async I/O context pub fn stream(&self) -> impl IsA { - // * do not replace with `tls_client_connection.base_io_stream()` - // as it will not work properly for user certificate sessions! match self.tls_client_connection.is_some() { + // is user session true => self .tls_client_connection .clone() .unwrap() - .upcast::(), // is user session - false => self.socket_connection.clone().upcast::(), // is guest session + .upcast::(), + // is guest session + false => self.socket_connection.clone().upcast::(), } } } -fn new_tls_client_connection( +// Helpers + +/// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) +/// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +pub fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, ) -> Result { @@ -110,6 +114,7 @@ fn new_tls_client_connection( // @TODO validate // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation tls_client_connection.connect_accept_certificate(|_, _, _| true); + Ok(tls_client_connection) } Err(e) => Err(Error::TlsClientConnection(e)), From a63e05685caef3b68e79f0f4b0d58543b284092c Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 09:29:26 +0200 Subject: [PATCH 207/392] update comment --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5acd3c0..3875873 100644 --- a/src/client.rs +++ b/src/client.rs @@ -44,8 +44,8 @@ impl Client { // Connect events socket.connect_event(|_, event, _, stream| { - // This condition have effect only for guest TLS connections - // * for user certificates validation, use `Connection` impl + // Condition applicable only for guest TLS connections + // * for user certificates validation, see `new_tls_client_connection` if event == SocketClientEvent::TlsHandshaking { // Begin guest certificate validation stream From 3791cbc4d0efbf5c25d92c449b9b749e02015e93 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 10:37:05 +0200 Subject: [PATCH 208/392] fix guest tls connection init --- src/client.rs | 4 ---- src/client/connection.rs | 37 ++++++++++++------------------------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3875873..8a2fa2c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -74,10 +74,6 @@ impl Client { certificate: Option, callback: impl Fn(Result) + 'static, ) { - // Toggle socket mode - // * guest sessions will not work without! - self.socket.set_tls(certificate.is_none()); - // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) diff --git a/src/client/connection.rs b/src/client/connection.rs index 2f6b128..62b0937 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -14,8 +14,7 @@ use glib::{ }; pub struct Connection { - pub socket_connection: SocketConnection, - pub tls_client_connection: Option, // is user certificate session + pub tls_client_connection: TlsClientConnection, } impl Connection { @@ -28,19 +27,18 @@ impl Connection { server_identity: Option, ) -> Result { Ok(Self { - tls_client_connection: match certificate { - Some(ref certificate) => { - match new_tls_client_connection(&socket_connection, server_identity.as_ref()) { - Ok(tls_client_connection) => { - tls_client_connection.set_certificate(certificate); - Some(tls_client_connection) - } - Err(e) => return Err(e), + tls_client_connection: match new_tls_client_connection( + &socket_connection, + server_identity.as_ref(), + ) { + Ok(tls_client_connection) => { + if let Some(ref certificate) = certificate { + tls_client_connection.set_certificate(certificate); } + tls_client_connection } - None => None, + Err(e) => return Err(e), }, - socket_connection, }) } @@ -75,22 +73,11 @@ impl Connection { // Getters - /// Cast [IOStream](https://docs.gtk.org/gio/class.IOStream.html) - /// for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) - /// or [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) (if available) + /// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// * compatible with user (certificate) and guest (certificate-less) connection type /// * useful to keep `Connection` reference active in async I/O context pub fn stream(&self) -> impl IsA { - match self.tls_client_connection.is_some() { - // is user session - true => self - .tls_client_connection - .clone() - .unwrap() - .upcast::(), - // is guest session - false => self.socket_connection.clone().upcast::(), - } + self.tls_client_connection.clone().upcast::() } } From 193dbef087cfbfc6e7d148cf88b07e066d67efd7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 10:41:01 +0200 Subject: [PATCH 209/392] return plain IOStream object --- src/client/connection.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 62b0937..72856b8 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -9,7 +9,7 @@ use gio::{ Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, }; use glib::{ - object::{Cast, IsA, ObjectExt}, + object::{Cast, ObjectExt}, Bytes, Priority, }; @@ -76,8 +76,9 @@ impl Connection { /// Get [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// * compatible with user (certificate) and guest (certificate-less) connection type /// * useful to keep `Connection` reference active in async I/O context - pub fn stream(&self) -> impl IsA { + pub fn stream(&self) -> IOStream { self.tls_client_connection.clone().upcast::() + // * also `base_io_stream` method available @TODO } } From 1a8bd44841d425b3e02e7297f961bd40a5265d23 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 10:44:04 +0200 Subject: [PATCH 210/392] update comments --- src/client/connection.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 72856b8..989f5ec 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -53,12 +53,14 @@ impl Connection { cancellable: Cancellable, callback: impl Fn(Result) + 'static, ) { + // Send request self.stream().output_stream().write_bytes_async( &Bytes::from(format!("{query}\r\n").as_bytes()), priority, Some(&cancellable.clone()), move |result| match result { Ok(_) => { + // Read response Response::from_connection_async(self, priority, cancellable, move |result| { callback(match result { Ok(response) => Ok(response), @@ -86,13 +88,14 @@ impl Connection { /// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) /// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) +/// using `server_identity` as [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) pub fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, ) -> Result { match TlsClientConnection::new(socket_connection, server_identity) { Ok(tls_client_connection) => { - // Prevent session resumption (on certificate change in runtime) + // Prevent session resumption (certificate change ability in runtime) tls_client_connection.set_property("session-resumption-enabled", false); // @TODO handle From 4c0fea0e99fb55b98e4d321b9cb75e47bc3ead13 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 10:50:25 +0200 Subject: [PATCH 211/392] remove deprecated event listener --- src/client.rs | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8a2fa2c..0f10be3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,12 +7,8 @@ pub mod error; pub use connection::Connection; pub use error::Error; -use gio::{ - prelude::{SocketClientExt, TlsConnectionExt}, - Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificate, - TlsClientConnection, -}; -use glib::{object::Cast, Priority, Uri}; +use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate}; +use glib::{Priority, Uri}; pub const DEFAULT_TIMEOUT: u32 = 10; @@ -42,20 +38,6 @@ impl Client { socket.set_protocol(SocketProtocol::Tcp); socket.set_timeout(DEFAULT_TIMEOUT); - // Connect events - socket.connect_event(|_, event, _, stream| { - // Condition applicable only for guest TLS connections - // * for user certificates validation, see `new_tls_client_connection` - if event == SocketClientEvent::TlsHandshaking { - // Begin guest certificate validation - stream - .unwrap() - .dynamic_cast_ref::() - .unwrap() - .connect_accept_certificate(|_, _, _| true); // @TODO - } - }); - // Done Self { socket } } From e9d51697bd3aa609d78338d6c7973ef5f279daea Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 10:52:25 +0200 Subject: [PATCH 212/392] remove extra comment --- src/client.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0f10be3..3aff592 100644 --- a/src/client.rs +++ b/src/client.rs @@ -65,21 +65,17 @@ impl Client { Some(&cancellable.clone()), move |result| match result { Ok(socket_connection) => { - // Wrap required connection dependencies into the struct holder match Connection::new(socket_connection, certificate, Some(network_address)) { - Ok(connection) => { - // Begin new request - connection.request_async( - uri.to_string(), - priority, - cancellable, - move |result| match result { - Ok(response) => callback(Ok(response)), - Err(e) => callback(Err(Error::Connection(e))), - }, - ) - } + Ok(connection) => connection.request_async( + uri.to_string(), + priority, + cancellable, + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), + }, + ), Err(e) => callback(Err(Error::Connection(e))), } } From 66245ef8dcc864e7d09aa4ecaf31d36db7b05d0a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 11:05:22 +0200 Subject: [PATCH 213/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 3aff592..6bf1872 100644 --- a/src/client.rs +++ b/src/client.rs @@ -44,7 +44,7 @@ impl Client { // Actions - /// High-level method make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), + /// Make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), /// callback with new `Response`on success or `Error` on failure /// * compatible with user (certificate) and guest (certificate-less) connection types /// * disables default `session-resumption-enabled` property to apply certificate change ability in runtime From 11313eafb97026a207a19879dfea4229cac5f836 Mon Sep 17 00:00:00 2001 From: d47081 <108541346+d47081@users.noreply.github.com> Date: Sun, 1 Dec 2024 11:49:22 +0200 Subject: [PATCH 214/392] Create rust.yml --- .github/workflows/rust.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..9fd45e0 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose From 096bd1d8620065b290be1e31ffefe0030e45e6ef Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 11:51:10 +0200 Subject: [PATCH 215/392] rename integration file --- tests/{client.rs => integration.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{client.rs => integration.rs} (100%) diff --git a/tests/client.rs b/tests/integration.rs similarity index 100% rename from tests/client.rs rename to tests/integration.rs From 94d63bd6de7719e7ef4483dcbab8bbb86a157651 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 12:57:54 +0200 Subject: [PATCH 216/392] make session resumption optional --- src/client.rs | 61 ++++++++++++++++++++++++++-------------- src/client/connection.rs | 5 +++- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/client.rs b/src/client.rs index 6bf1872..0264be9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,13 +10,17 @@ pub use error::Error; use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate}; use glib::{Priority, Uri}; +// Defaults + pub const DEFAULT_TIMEOUT: u32 = 10; +pub const DEFAULT_SESSION_RESUMPTION: bool = false; /// Main point where connect external crate /// /// Provides high-level API for session-safe interaction with /// [Gemini](https://geminiprotocol.net) socket server pub struct Client { + is_session_resumption: bool, pub socket: SocketClient, } @@ -39,7 +43,10 @@ impl Client { socket.set_timeout(DEFAULT_TIMEOUT); // Done - Self { socket } + Self { + is_session_resumption: DEFAULT_SESSION_RESUMPTION, + socket, + } } // Actions @@ -60,29 +67,41 @@ impl Client { // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { - Ok(network_address) => self.socket.connect_async( - &network_address.clone(), - Some(&cancellable.clone()), - move |result| match result { - Ok(socket_connection) => { - match Connection::new(socket_connection, certificate, Some(network_address)) - { - Ok(connection) => connection.request_async( - uri.to_string(), - priority, - cancellable, - move |result| match result { - Ok(response) => callback(Ok(response)), + Ok(network_address) => { + self.socket + .connect_async(&network_address.clone(), Some(&cancellable.clone()), { + let is_session_resumption = self.is_session_resumption; + move |result| match result { + Ok(socket_connection) => { + match Connection::new( + socket_connection, + certificate, + Some(network_address), + is_session_resumption, + ) { + Ok(connection) => connection.request_async( + uri.to_string(), + priority, + cancellable, + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), + }, + ), Err(e) => callback(Err(Error::Connection(e))), - }, - ), - Err(e) => callback(Err(Error::Connection(e))), + } + } + Err(e) => callback(Err(Error::Connect(e))), } - } - Err(e) => callback(Err(Error::Connect(e))), - }, - ), + }) + } Err(e) => callback(Err(Error::NetworkAddress(e))), } } + + // Setters + + pub fn set_session_resumption(&mut self, is_enabled: bool) { + self.is_session_resumption = is_enabled + } } diff --git a/src/client/connection.rs b/src/client/connection.rs index 989f5ec..d5588bf 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -25,11 +25,13 @@ impl Connection { socket_connection: SocketConnection, certificate: Option, server_identity: Option, + is_session_resumption: bool, ) -> Result { Ok(Self { tls_client_connection: match new_tls_client_connection( &socket_connection, server_identity.as_ref(), + is_session_resumption, ) { Ok(tls_client_connection) => { if let Some(ref certificate) = certificate { @@ -92,11 +94,12 @@ impl Connection { pub fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, + is_session_resumption: bool, ) -> Result { match TlsClientConnection::new(socket_connection, server_identity) { Ok(tls_client_connection) => { // Prevent session resumption (certificate change ability in runtime) - tls_client_connection.set_property("session-resumption-enabled", false); + tls_client_connection.set_property("session-resumption-enabled", is_session_resumption); // @TODO handle // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections From ce19e94db96c812b35ddfacdf1cc6198a4dd6c67 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 13:01:30 +0200 Subject: [PATCH 217/392] add comment --- src/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 0264be9..d66afa7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -54,7 +54,6 @@ impl Client { /// Make new async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html), /// callback with new `Response`on success or `Error` on failure /// * compatible with user (certificate) and guest (certificate-less) connection types - /// * disables default `session-resumption-enabled` property to apply certificate change ability in runtime pub fn request_async( &self, uri: Uri, @@ -101,6 +100,8 @@ impl Client { // Setters + /// Change `session-resumption-enabled` property to apply new certificate option in runtime + /// * disabled by default pub fn set_session_resumption(&mut self, is_enabled: bool) { self.is_session_resumption = is_enabled } From 3fbd6ff3e300138f3f2734752d28993fe0285c39 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 13:02:34 +0200 Subject: [PATCH 218/392] update comment --- src/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index d66afa7..ae9f474 100644 --- a/src/client.rs +++ b/src/client.rs @@ -100,7 +100,8 @@ impl Client { // Setters - /// Change `session-resumption-enabled` property to apply new certificate option in runtime + /// Change `session-resumption-enabled` property value to apply new certificate option in runtime + /// https://www.gnutls.org/manual/html_node/Session-resumption.html /// * disabled by default pub fn set_session_resumption(&mut self, is_enabled: bool) { self.is_session_resumption = is_enabled From 41d7d8e4f39809332d8428a034cd2ad8199cd8eb Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 13:09:22 +0200 Subject: [PATCH 219/392] update comment --- src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index ae9f474..85dcfbb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -100,9 +100,9 @@ impl Client { // Setters - /// Change `session-resumption-enabled` property value to apply new certificate option in runtime - /// https://www.gnutls.org/manual/html_node/Session-resumption.html - /// * disabled by default + /// Change `session-resumption-enabled` property (`false` by default) + /// * [Gemini specification](https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates) + /// * [GnuTLS manual](https://www.gnutls.org/manual/html_node/Session-resumption.html) pub fn set_session_resumption(&mut self, is_enabled: bool) { self.is_session_resumption = is_enabled } From d313f900ba85a223960140c1de562f16b405fff2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 13:12:07 +0200 Subject: [PATCH 220/392] update comment --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 85dcfbb..77026e9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -100,7 +100,7 @@ impl Client { // Setters - /// Change `session-resumption-enabled` property (`false` by default) + /// Change glib-networking `session-resumption-enabled` property (`false` by default) /// * [Gemini specification](https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates) /// * [GnuTLS manual](https://www.gnutls.org/manual/html_node/Session-resumption.html) pub fn set_session_resumption(&mut self, is_enabled: bool) { From a77e4abf507a7b7755a7ee6d43dec88485b6f460 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 13:43:07 +0200 Subject: [PATCH 221/392] add usage example --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 5eb2487..60746a0 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,45 @@ cargo add ggemini * [Documentation](https://docs.rs/ggemini/latest/ggemini/) +### Example + +``` rust +use gtk::gio::*; +use gtk::glib::*; + +use ggemini::client::{ + connection::{ + response::meta::{Mime, Status}, + Response, + }, + Client, Error, +}; + +fn main() -> ExitCode { + Client::new().request_async( + Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), + Priority::DEFAULT, + Cancellable::new(), + None, // optional `GTlsCertificate` + |result: Result| match result { + Ok(response) => { + match response.meta.status { + // route by status code + Status::Success => match response.meta.mime { + // handle `GIOStream` by content type + Some(Mime::TextGemini) => todo!(), + _ => todo!(), + }, + _ => todo!(), + } + } + Err(e) => todo!("{e}"), + }, + ); + ExitCode::SUCCESS +} +``` + ## See also * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file From 7802ffad677c2edebf23cf8fd35b9286dab82f6a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 13:47:54 +0200 Subject: [PATCH 222/392] change version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 538c997..5d22ad6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.11.0" +version = "0.11.1" edition = "2021" license = "MIT" readme = "README.md" From 3c627d6e4b665ace6c74240e7c7a23779c68da60 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 13:49:55 +0200 Subject: [PATCH 223/392] rollback to release version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5d22ad6..538c997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.11.1" +version = "0.11.0" edition = "2021" license = "MIT" readme = "README.md" From 128b5d68b6e46eb6f417d9f355650c32aecfcc41 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 1 Dec 2024 13:50:44 +0200 Subject: [PATCH 224/392] change version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 538c997..5d22ad6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.11.0" +version = "0.11.1" edition = "2021" license = "MIT" readme = "README.md" From f0f34dfdb274644cb6232a820128805fca6106cd Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 2 Dec 2024 13:50:21 +0200 Subject: [PATCH 225/392] update error message --- src/client/connection/response/meta/mime/error.rs | 2 +- src/client/connection/response/meta/status/error.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs index 4b66ed2..b535376 100644 --- a/src/client/connection/response/meta/mime/error.rs +++ b/src/client/connection/response/meta/mime/error.rs @@ -17,7 +17,7 @@ impl Display for Error { write!(f, "Protocol error") } Self::Undefined => { - write!(f, "Undefined error") + write!(f, "Undefined") } } } diff --git a/src/client/connection/response/meta/status/error.rs b/src/client/connection/response/meta/status/error.rs index 4b66ed2..b535376 100644 --- a/src/client/connection/response/meta/status/error.rs +++ b/src/client/connection/response/meta/status/error.rs @@ -17,7 +17,7 @@ impl Display for Error { write!(f, "Protocol error") } Self::Undefined => { - write!(f, "Undefined error") + write!(f, "Undefined") } } } From 18806e2f383604d323ffcdc4bfd17c7b97989235 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 3 Dec 2024 20:15:07 +0200 Subject: [PATCH 226/392] update comments --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60746a0..2998777 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,11 @@ fn main() -> ExitCode { None, // optional `GTlsCertificate` |result: Result| match result { Ok(response) => { + // route by status code match response.meta.status { - // route by status code + // is code 20, handle `GIOStream` by content type Status::Success => match response.meta.mime { - // handle `GIOStream` by content type + // is gemtext, see also ggemtext crate! Some(Mime::TextGemini) => todo!(), _ => todo!(), }, From 1f05ccc149c40e833965b6888667fd35bc3ef1c4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 4 Dec 2024 05:56:07 +0200 Subject: [PATCH 227/392] add rustfmt, clippy validation --- .github/workflows/rust.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9fd45e0..e4002e2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,6 +16,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Run rustfmt + run: cargo fmt --all -- --check + - name: Run clippy + run: cargo clippy --all-targets - name: Build run: cargo build --verbose - name: Run tests From 6ac26bad62f204222628a86e629aa925ccae2161 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 7 Dec 2024 22:52:12 +0200 Subject: [PATCH 228/392] validate warnings --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e4002e2..9f5f88e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,6 +8,7 @@ on: env: CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings jobs: build: From 059fa8f2d7465c34369f52043b1dbfe27be1fb4c Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 9 Dec 2024 14:00:35 +0200 Subject: [PATCH 229/392] rename method from `read_all_from_stream_async` to `move_all_from_stream_async`, change arguments order, update comments --- src/gio/memory_input_stream.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index dbb0bc3..aa67d48 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -22,9 +22,9 @@ pub fn from_stream_async( on_chunk: impl Fn((Bytes, usize)) + 'static, on_complete: impl FnOnce(Result) + 'static, ) { - read_all_from_stream_async( - MemoryInputStream::new(), + move_all_from_stream_async( base_io_stream, + MemoryInputStream::new(), cancelable, priority, (bytes_in_chunk, bytes_total_limit, 0), @@ -32,18 +32,22 @@ pub fn from_stream_async( ); } -/// Asynchronously read entire [InputStream](https://docs.gtk.org/gio/class.InputStream.html) -/// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// to [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread -pub fn read_all_from_stream_async( - memory_input_stream: MemoryInputStream, +pub fn move_all_from_stream_async( base_io_stream: impl IsA, - cancelable: Cancellable, + memory_input_stream: MemoryInputStream, + cancellable: Cancellable, priority: Priority, - bytes: (usize, usize, usize), + bytes: ( + usize, // bytes_in_chunk + usize, // bytes_total_limit + usize, // bytes_total + ), callback: ( - impl Fn((Bytes, usize)) + 'static, - impl FnOnce(Result) + 'static, + impl Fn((Bytes, usize)) + 'static, // on_chunk + impl FnOnce(Result) + 'static, // on_complete ), ) { let (on_chunk, on_complete) = callback; @@ -52,7 +56,7 @@ pub fn read_all_from_stream_async( base_io_stream.input_stream().read_bytes_async( bytes_in_chunk, priority, - Some(&cancelable.clone()), + Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { // Update bytes total @@ -75,10 +79,10 @@ pub fn read_all_from_stream_async( memory_input_stream.add_bytes(&bytes); // Continue - read_all_from_stream_async( - memory_input_stream, + move_all_from_stream_async( base_io_stream, - cancelable, + memory_input_stream, + cancellable, priority, (bytes_in_chunk, bytes_total_limit, bytes_total), (on_chunk, on_complete), From 55ee734a0b68d868485039cd5d5b71d3b10a07d7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 9 Dec 2024 14:01:19 +0200 Subject: [PATCH 230/392] add FileOutputStream API --- src/gio.rs | 1 + src/gio/file_output_stream.rs | 83 +++++++++++++++++++++++++++++ src/gio/file_output_stream/error.rs | 24 +++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/gio/file_output_stream.rs create mode 100644 src/gio/file_output_stream/error.rs diff --git a/src/gio.rs b/src/gio.rs index 8206018..72a89d8 100644 --- a/src/gio.rs +++ b/src/gio.rs @@ -1,2 +1,3 @@ +pub mod file_output_stream; pub mod memory_input_stream; pub mod network_address; diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs new file mode 100644 index 0000000..cfb8f78 --- /dev/null +++ b/src/gio/file_output_stream.rs @@ -0,0 +1,83 @@ +pub mod error; +pub use error::Error; + +use gio::{ + prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual}, + Cancellable, FileOutputStream, IOStream, +}; +use glib::{object::IsA, Bytes, Priority}; + +/// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) +/// * require `IOStream` reference to keep `Connection` active in async thread +pub fn move_all_from_stream_async( + base_io_stream: impl IsA, + file_output_stream: FileOutputStream, + cancellable: Cancellable, + priority: Priority, + bytes: ( + usize, // bytes_in_chunk + usize, // bytes_total_limit + usize, // bytes_total + ), + callback: ( + impl Fn((Bytes, usize)) + 'static, // on_chunk + impl FnOnce(Result) + 'static, // on_complete + ), +) { + let (on_chunk, on_complete) = callback; + let (bytes_in_chunk, bytes_total_limit, bytes_total) = bytes; + + base_io_stream.input_stream().read_bytes_async( + bytes_in_chunk, + priority, + Some(&cancellable.clone()), + move |result| match result { + Ok(bytes) => { + // Update bytes total + let bytes_total = bytes_total + bytes.len(); + + // Callback chunk function + on_chunk((bytes.clone(), bytes_total)); + + // Validate max size + if bytes_total > bytes_total_limit { + return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); + } + + // No bytes were read, end of stream + if bytes.len() == 0 { + return on_complete(Ok(file_output_stream)); + } + + // Write chunk bytes + file_output_stream.clone().write_async( + bytes.clone(), + priority, + Some(&cancellable.clone()), + move |result| { + match result { + Ok(_) => { + // Continue + move_all_from_stream_async( + base_io_stream, + file_output_stream, + cancellable, + priority, + (bytes_in_chunk, bytes_total_limit, bytes_total), + (on_chunk, on_complete), + ); + } + Err((bytes, e)) => { + on_complete(Err(Error::OutputStream(bytes.clone(), e))) + } + } + }, + ); + } + Err(e) => { + on_complete(Err(Error::InputStream(e))); + } + }, + ); +} diff --git a/src/gio/file_output_stream/error.rs b/src/gio/file_output_stream/error.rs new file mode 100644 index 0000000..6b1ee76 --- /dev/null +++ b/src/gio/file_output_stream/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + BytesTotal(usize, usize), + InputStream(glib::Error), + OutputStream(glib::Bytes, glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::BytesTotal(total, limit) => { + write!(f, "Bytes total limit reached: {total} / {limit}") + } + Self::InputStream(e) => { + write!(f, "Input stream error: {e}") + } + Self::OutputStream(_, e) => { + write!(f, "Output stream error: {e}") + } + } + } +} From 8c298977f3df13b153553caa696d9785827e1832 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 9 Dec 2024 14:01:45 +0200 Subject: [PATCH 231/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5d22ad6..5fd1e7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.11.1" +version = "0.12.0" edition = "2021" license = "MIT" readme = "README.md" From eb32db3d3bde6e2ca8d2eaa33dc4df46ee961605 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 9 Dec 2024 14:14:01 +0200 Subject: [PATCH 232/392] make `bytes_total_limit` optional --- src/gio/file_output_stream.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index cfb8f78..3eedc36 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -16,9 +16,9 @@ pub fn move_all_from_stream_async( cancellable: Cancellable, priority: Priority, bytes: ( - usize, // bytes_in_chunk - usize, // bytes_total_limit - usize, // bytes_total + usize, // bytes_in_chunk + Option, // bytes_total_limit, `None` to unlimited + usize, // bytes_total ), callback: ( impl Fn((Bytes, usize)) + 'static, // on_chunk @@ -41,8 +41,10 @@ pub fn move_all_from_stream_async( on_chunk((bytes.clone(), bytes_total)); // Validate max size - if bytes_total > bytes_total_limit { - return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); + if let Some(bytes_total_limit) = bytes_total_limit { + if bytes_total > bytes_total_limit { + return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); + } } // No bytes were read, end of stream From f33e53e51bf2f8d80a952858f4a5a9f329993c10 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 9 Dec 2024 14:33:51 +0200 Subject: [PATCH 233/392] remove extra tuple --- src/gio/file_output_stream.rs | 4 ++-- src/gio/memory_input_stream.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 3eedc36..c4a1e2d 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -21,7 +21,7 @@ pub fn move_all_from_stream_async( usize, // bytes_total ), callback: ( - impl Fn((Bytes, usize)) + 'static, // on_chunk + impl Fn(Bytes, usize) + 'static, // on_chunk impl FnOnce(Result) + 'static, // on_complete ), ) { @@ -38,7 +38,7 @@ pub fn move_all_from_stream_async( let bytes_total = bytes_total + bytes.len(); // Callback chunk function - on_chunk((bytes.clone(), bytes_total)); + on_chunk(bytes.clone(), bytes_total); // Validate max size if let Some(bytes_total_limit) = bytes_total_limit { diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index aa67d48..1ab065f 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -19,7 +19,7 @@ pub fn from_stream_async( priority: Priority, bytes_in_chunk: usize, bytes_total_limit: usize, - on_chunk: impl Fn((Bytes, usize)) + 'static, + on_chunk: impl Fn(Bytes, usize) + 'static, on_complete: impl FnOnce(Result) + 'static, ) { move_all_from_stream_async( @@ -46,7 +46,7 @@ pub fn move_all_from_stream_async( usize, // bytes_total ), callback: ( - impl Fn((Bytes, usize)) + 'static, // on_chunk + impl Fn(Bytes, usize) + 'static, // on_chunk impl FnOnce(Result) + 'static, // on_complete ), ) { @@ -63,7 +63,7 @@ pub fn move_all_from_stream_async( let bytes_total = bytes_total + bytes.len(); // Callback chunk function - on_chunk((bytes.clone(), bytes_total)); + on_chunk(bytes.clone(), bytes_total); // Validate max size if bytes_total > bytes_total_limit { From 1911b0ad95d9dff2239d12996cf4679bc62e608c Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 9 Dec 2024 14:50:31 +0200 Subject: [PATCH 234/392] return bytes total `on_complete` --- src/gio/file_output_stream.rs | 6 +++--- src/gio/memory_input_stream.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index c4a1e2d..b34b507 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -21,8 +21,8 @@ pub fn move_all_from_stream_async( usize, // bytes_total ), callback: ( - impl Fn(Bytes, usize) + 'static, // on_chunk - impl FnOnce(Result) + 'static, // on_complete + impl Fn(Bytes, usize) + 'static, // on_chunk + impl FnOnce(Result<(FileOutputStream, usize), Error>) + 'static, // on_complete ), ) { let (on_chunk, on_complete) = callback; @@ -49,7 +49,7 @@ pub fn move_all_from_stream_async( // No bytes were read, end of stream if bytes.len() == 0 { - return on_complete(Ok(file_output_stream)); + return on_complete(Ok((file_output_stream, bytes_total))); } // Write chunk bytes diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 1ab065f..30f9fd8 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -20,7 +20,7 @@ pub fn from_stream_async( bytes_in_chunk: usize, bytes_total_limit: usize, on_chunk: impl Fn(Bytes, usize) + 'static, - on_complete: impl FnOnce(Result) + 'static, + on_complete: impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ) { move_all_from_stream_async( base_io_stream, @@ -46,8 +46,8 @@ pub fn move_all_from_stream_async( usize, // bytes_total ), callback: ( - impl Fn(Bytes, usize) + 'static, // on_chunk - impl FnOnce(Result) + 'static, // on_complete + impl Fn(Bytes, usize) + 'static, // on_chunk + impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, // on_complete ), ) { let (on_chunk, on_complete) = callback; @@ -72,7 +72,7 @@ pub fn move_all_from_stream_async( // No bytes were read, end of stream if bytes.len() == 0 { - return on_complete(Ok(memory_input_stream)); + return on_complete(Ok((memory_input_stream, bytes_total))); } // Write chunk bytes From e76bc62d8234238490e31848bc6d19422ec0aba7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 11 Dec 2024 10:19:14 +0200 Subject: [PATCH 235/392] add mime type groups support --- src/client/connection/response/meta/mime.rs | 34 +++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index c86d627..49fbf36 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -14,19 +14,28 @@ use std::path::Path; /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters #[derive(Debug)] pub enum Mime { - // Text + // text TextGemini, TextPlain, - // Image + /// Match unlisted `text/*` + Text, + // image ImageGif, ImageJpeg, ImagePng, ImageSvg, ImageWebp, - // Audio + /// Match unlisted `image/*` + Image, + // audio AudioFlac, AudioMpeg, AudioOgg, + /// Match unlisted `audio/*` + Audio, + // other + /// Match unlisted `application/*` + Application, } // @TODO impl Mime { @@ -91,6 +100,10 @@ impl Mime { return Ok(Some(Self::TextPlain)); } + if value.contains("text/") { + return Ok(Some(Self::Text)); + } + // Image if value.contains("image/gif") { return Ok(Some(Self::ImageGif)); @@ -112,6 +125,10 @@ impl Mime { return Ok(Some(Self::ImageWebp)); } + if value.contains("image/") { + return Ok(Some(Self::Image)); + } + // Audio if value.contains("audio/flac") { return Ok(Some(Self::AudioFlac)); @@ -125,6 +142,17 @@ impl Mime { return Ok(Some(Self::AudioOgg)); } + if value.contains("audio/") { + return Ok(Some(Self::Audio)); + } + + // Other + if value.contains("application/") { + return Ok(Some(Self::Application)); + } + + // application/x-tar + // Some type exist, but not defined yet (on status code is 2*) if value.starts_with("2") && value.contains("/") { return Err(Error::Undefined); From 76235da239de707170c1abe69b7543355504609e Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 11 Dec 2024 10:24:08 +0200 Subject: [PATCH 236/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5fd1e7c..3ac706c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.12.0" +version = "0.12.1" edition = "2021" license = "MIT" readme = "README.md" From 2c88f12f2d5b15df50a5ee618d30012ba230fc3f Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 12 Dec 2024 10:00:41 +0200 Subject: [PATCH 237/392] dump unsupported mime type string --- src/client/connection/response/meta/mime.rs | 15 ++++++++++++--- src/client/connection/response/meta/mime/error.rs | 13 ++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 49fbf36..5aa362c 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -8,7 +8,7 @@ pub mod error; pub use error::Error; -use glib::{GString, Uri}; +use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags, Uri}; use std::path::Path; /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters @@ -80,7 +80,7 @@ impl Mime { Some("flac") => Ok(Self::AudioFlac), Some("mp3") => Ok(Self::AudioMpeg), Some("oga" | "ogg" | "opus" | "spx") => Ok(Self::AudioOgg), - _ => Err(Error::Undefined), + _ => Err(Error::Undefined(None)), } // @TODO extension to lowercase } @@ -155,7 +155,16 @@ impl Mime { // Some type exist, but not defined yet (on status code is 2*) if value.starts_with("2") && value.contains("/") { - return Err(Error::Undefined); + return Err(Error::Undefined( + Regex::split_simple( + r"^2\d{1}\s([^\/]+\/[^\s]+)", + value, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ) + .get(1) + .map(|this| this.to_string()), + )); } // Done diff --git a/src/client/connection/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs index b535376..d79790a 100644 --- a/src/client/connection/response/meta/mime/error.rs +++ b/src/client/connection/response/meta/mime/error.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result}; pub enum Error { Decode(std::string::FromUtf8Error), Protocol, - Undefined, + Undefined(Option), } impl Display for Error { @@ -16,8 +16,15 @@ impl Display for Error { Self::Protocol => { write!(f, "Protocol error") } - Self::Undefined => { - write!(f, "Undefined") + Self::Undefined(e) => { + write!( + f, + "{}", + match e { + Some(value) => format!("`{value}` undefined"), + None => "Could not parse value".to_string(), + } + ) } } } From 249199f780e91de5e0ca0c77cb364a4654975716 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 12 Dec 2024 10:01:10 +0200 Subject: [PATCH 238/392] change api version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3ac706c..e2141e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "MIT" readme = "README.md" From bdc2b5094068d368e9941d3bc4b6064687dc3ef7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 12 Dec 2024 11:58:18 +0200 Subject: [PATCH 239/392] remove mime type enumeration as external feature, parse and return raw string instead --- src/client/connection/response/meta/mime.rs | 174 +++--------------- .../connection/response/meta/mime/error.rs | 13 +- 2 files changed, 29 insertions(+), 158 deletions(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 5aa362c..4568ce8 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -1,50 +1,19 @@ -//! 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**) +//! MIME type parser for different data types pub mod error; pub use error::Error; -use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags, Uri}; -use std::path::Path; +use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters -#[derive(Debug)] -pub enum Mime { - // text - TextGemini, - TextPlain, - /// Match unlisted `text/*` - Text, - // image - ImageGif, - ImageJpeg, - ImagePng, - ImageSvg, - ImageWebp, - /// Match unlisted `image/*` - Image, - // audio - AudioFlac, - AudioMpeg, - AudioOgg, - /// Match unlisted `audio/*` - Audio, - // other - /// Match unlisted `application/*` - Application, -} // @TODO + +pub struct Mime { + pub value: String, +} 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) + /// * return `None` for non 2* [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) pub fn from_utf8(buffer: &[u8]) -> Result, Error> { // Define max buffer length for this method const MAX_LEN: usize = 0x400; // 1024 @@ -62,118 +31,27 @@ impl Mime { } } - /// Create new `Self` from `std::Path` that includes file **extension** - pub fn from_path(path: &Path) -> Result { - 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(None)), - } // @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, Error> { - // Text - if value.contains("text/gemini") { - return Ok(Some(Self::TextGemini)); + /// * return `None` for non 2* [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) + pub fn from_string(subject: &str) -> Result, Error> { + if !subject.starts_with("2") { + return Ok(None); } - - if value.contains("text/plain") { - return Ok(Some(Self::TextPlain)); + match parse(subject) { + Some(value) => Ok(Some(Self { value })), + None => Err(Error::Undefined), } - - if value.contains("text/") { - return Ok(Some(Self::Text)); - } - - // 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)); - } - - if value.contains("image/") { - return Ok(Some(Self::Image)); - } - - // 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)); - } - - if value.contains("audio/") { - return Ok(Some(Self::Audio)); - } - - // Other - if value.contains("application/") { - return Ok(Some(Self::Application)); - } - - // application/x-tar - - // Some type exist, but not defined yet (on status code is 2*) - if value.starts_with("2") && value.contains("/") { - return Err(Error::Undefined( - Regex::split_simple( - r"^2\d{1}\s([^\/]+\/[^\s]+)", - value, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ) - .get(1) - .map(|this| this.to_string()), - )); - } - - // 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::from_path(Path::new(&uri.to_string())) } } + +/// Extract MIME type from from string that includes **header** +pub fn parse(value: &str) -> Option { + Regex::split_simple( + r"^2\d{1}\s([^\/]+\/[^\s;]+)", + value, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ) + .get(1) + .map(|this| this.to_string()) +} diff --git a/src/client/connection/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs index d79790a..9ea72ba 100644 --- a/src/client/connection/response/meta/mime/error.rs +++ b/src/client/connection/response/meta/mime/error.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result}; pub enum Error { Decode(std::string::FromUtf8Error), Protocol, - Undefined(Option), + Undefined, } impl Display for Error { @@ -16,15 +16,8 @@ impl Display for Error { Self::Protocol => { write!(f, "Protocol error") } - Self::Undefined(e) => { - write!( - f, - "{}", - match e { - Some(value) => format!("`{value}` undefined"), - None => "Could not parse value".to_string(), - } - ) + Self::Undefined => { + write!(f, "MIME type undefined") } } } From 7e9c574d4a19fdcb8739057f580927aa2b2ac786 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 14 Dec 2024 06:23:10 +0200 Subject: [PATCH 240/392] remove extra line --- src/client/connection/response/meta/mime.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 4568ce8..6378874 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -6,7 +6,6 @@ pub use error::Error; use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters - pub struct Mime { pub value: String, } From 54fabd241b01bd2a00e2abcd27d5e65a312357d4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 14 Dec 2024 06:28:48 +0200 Subject: [PATCH 241/392] update example with new api version --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2998777..f6561e7 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,10 @@ fn main() -> ExitCode { // route by status code match response.meta.status { // is code 20, handle `GIOStream` by content type - Status::Success => match response.meta.mime { - // is gemtext, see also ggemtext crate! - Some(Mime::TextGemini) => todo!(), + Status::Success => match response.meta.mime.unwrap().value.as_str() { + // is gemtext, see ggemtext crate to parse + "text/gemini" => todo!(), + // other types _ => todo!(), }, _ => todo!(), From 87ccca537316fc1cae9e1d68f568b971567483f8 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 14 Dec 2024 10:29:46 +0200 Subject: [PATCH 242/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e2141e6..3672027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "MIT" readme = "README.md" From 256939c3b49290d84c54cbea4fa457fc73f1cbc1 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 20 Dec 2024 10:22:35 +0200 Subject: [PATCH 243/392] rename workflow --- .github/workflows/{rust.yml => build.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{rust.yml => build.yml} (97%) diff --git a/.github/workflows/rust.yml b/.github/workflows/build.yml similarity index 97% rename from .github/workflows/rust.yml rename to .github/workflows/build.yml index 9f5f88e..85edd8b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Rust +name: Build on: push: From e2bee95140ba79ea758fa38bdb7e186409ac28ed Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 20 Dec 2024 10:22:50 +0200 Subject: [PATCH 244/392] add badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f6561e7..920a9f6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ggemini +![Build](https://github.com/YGGverse/ggemini/actions/workflows/build.yml/badge.svg) + Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > [!IMPORTANT] From 66a0de6a8e67602c2216cc6dcbe02103b71dbe02 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 21 Dec 2024 20:28:40 +0200 Subject: [PATCH 245/392] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 920a9f6..1c455fb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # ggemini ![Build](https://github.com/YGGverse/ggemini/actions/workflows/build.yml/badge.svg) +[![Documentation](https://docs.rs/ggemini/badge.svg)](https://docs.rs/ggemini) +[![crates.io](https://img.shields.io/crates/v/ggemini.svg)](https://crates.io/crates/ggemini) Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) From 29b835411d83146bdba05616b0a765b0ba809691 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:22:03 +0200 Subject: [PATCH 246/392] implement Titan protocol features --- Cargo.toml | 4 +- README.md | 19 +++++--- src/client.rs | 46 ++++++++++++------ src/client/connection.rs | 63 +++++++++++++++++++++++-- src/client/connection/request.rs | 10 ++++ src/client/connection/request/gemini.rs | 22 +++++++++ src/client/connection/request/titan.rs | 53 +++++++++++++++++++++ tests/integration.rs | 14 ++++++ 8 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 src/client/connection/request.rs create mode 100644 src/client/connection/request/gemini.rs create mode 100644 src/client/connection/request/titan.rs diff --git a/Cargo.toml b/Cargo.toml index 3672027..b19da7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "ggemini" -version = "0.13.1" +version = "0.14.0" edition = "2021" license = "MIT" readme = "README.md" description = "Glib/Gio-oriented network API for Gemini protocol" -keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"] +keywords = ["gemini", "titan", "glib", "gio", "client"] categories = ["development-tools", "network-programming", "parsing"] repository = "https://github.com/YGGverse/ggemini" diff --git a/README.md b/README.md index 1c455fb..f49ce36 100644 --- a/README.md +++ b/README.md @@ -25,20 +25,25 @@ cargo add ggemini ### Example ``` rust -use gtk::gio::*; -use gtk::glib::*; +use gio::*; +use glib::*; use ggemini::client::{ connection::{ - response::meta::{Mime, Status}, - Response, + Request, Response, + request::Gemini, + response::meta::{Mime, Status} }, Client, Error, }; fn main() -> ExitCode { Client::new().request_async( - Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), + Request::Gemini( + Gemini::build( + Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap() + ) + ), Priority::DEFAULT, Cancellable::new(), None, // optional `GTlsCertificate` @@ -63,6 +68,8 @@ fn main() -> ExitCode { } ``` -## See also +* to send requests using Titan protocol, see also `titan_request_async` implementation + +## Other crates * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index 77026e9..c92077a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,11 +4,11 @@ pub mod connection; pub mod error; -pub use connection::Connection; +pub use connection::{Connection, Request, Response}; pub use error::Error; use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate}; -use glib::{Priority, Uri}; +use glib::Priority; // Defaults @@ -56,16 +56,22 @@ impl Client { /// * compatible with user (certificate) and guest (certificate-less) connection types pub fn request_async( &self, - uri: Uri, + request: Request, priority: Priority, cancellable: Cancellable, certificate: Option, - callback: impl Fn(Result) + 'static, + callback: impl Fn(Result) + 'static, ) { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) - match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { + match crate::gio::network_address::from_uri( + &match request { + Request::Gemini(ref request) => request.uri.clone(), + Request::Titan(ref request) => request.uri.clone(), + }, + crate::DEFAULT_PORT, + ) { Ok(network_address) => { self.socket .connect_async(&network_address.clone(), Some(&cancellable.clone()), { @@ -78,15 +84,27 @@ impl Client { Some(network_address), is_session_resumption, ) { - Ok(connection) => connection.request_async( - uri.to_string(), - priority, - cancellable, - move |result| match result { - Ok(response) => callback(Ok(response)), - Err(e) => callback(Err(Error::Connection(e))), - }, - ), + Ok(connection) => match request { + Request::Gemini(request) => connection + .gemini_request_async( + request, + priority, + cancellable, + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), + }, + ), + Request::Titan(request) => connection.titan_request_async( + request, + priority, + cancellable, + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), + }, + ), + }, Err(e) => callback(Err(Error::Connection(e))), } } diff --git a/src/client/connection.rs b/src/client/connection.rs index d5588bf..380a115 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,7 +1,9 @@ pub mod error; +pub mod request; pub mod response; pub use error::Error; +pub use request::{Gemini, Request, Titan}; pub use response::Response; use gio::{ @@ -46,18 +48,69 @@ impl Connection { // Actions - /// Make new request to `Self` connection - /// * callback with new `Response` on success or `Error` on failure + /// Send new `Request` to `Self` connection using + /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or + /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol pub fn request_async( self, - query: String, + request: Request, + priority: Priority, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, + ) { + match request { + Request::Gemini(request) => { + self.gemini_request_async(request, priority, cancellable, callback) + } + Request::Titan(request) => { + self.titan_request_async(request, priority, cancellable, callback) + } + } + } + + /// Make new request to `Self` connection using + /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol + /// * callback with new `Response` on success or `Error` on failure + /// * see also `request_async` method to send multi-protocol requests + pub fn gemini_request_async( + self, + request: Gemini, + priority: Priority, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, + ) { + self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); + } + + /// Make new request to `Self` connection using + /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol + /// * callback with new `Response` on success or `Error` on failure + /// * see also `request_async` method to send multi-protocol requests + pub fn titan_request_async( + self, + request: Titan, + priority: Priority, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, + ) { + self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); + } + + /// Low-level shared method to send raw bytes array over + /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or + /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol + /// * bytes array should include formatted header according to protocol selected + /// * for high-level requests see `gemini_request_async` and `titan_request_async` methods + /// * to construct multi-protocol request with single function, use `request_async` method + pub fn bytes_request_async( + self, + request: &Bytes, priority: Priority, cancellable: Cancellable, callback: impl Fn(Result) + 'static, ) { - // Send request self.stream().output_stream().write_bytes_async( - &Bytes::from(format!("{query}\r\n").as_bytes()), + request, priority, Some(&cancellable.clone()), move |result| match result { diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs new file mode 100644 index 0000000..5986f7b --- /dev/null +++ b/src/client/connection/request.rs @@ -0,0 +1,10 @@ +pub mod gemini; +pub mod titan; + +pub use gemini::Gemini; +pub use titan::Titan; + +pub enum Request { + Gemini(Gemini), + Titan(Titan), +} diff --git a/src/client/connection/request/gemini.rs b/src/client/connection/request/gemini.rs new file mode 100644 index 0000000..4887499 --- /dev/null +++ b/src/client/connection/request/gemini.rs @@ -0,0 +1,22 @@ +use glib::{Bytes, Uri}; + +/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol enum object for `Request` +pub struct Gemini { + pub uri: Uri, +} + +impl Gemini { + // Constructors + + /// Build valid new `Self` + pub fn build(uri: Uri) -> Self { + Self { uri } // @TODO validate + } + + // Getters + + /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + pub fn to_bytes(&self) -> Bytes { + Bytes::from(format!("{}\r\n", self.uri).as_bytes()) + } +} diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs new file mode 100644 index 0000000..0affd3f --- /dev/null +++ b/src/client/connection/request/titan.rs @@ -0,0 +1,53 @@ +use glib::{Bytes, Uri}; + +/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol enum object for `Request` +pub struct Titan { + pub uri: Uri, + pub size: usize, + pub mime: String, + pub token: Option, + pub data: Vec, +} + +impl Titan { + // Constructors + + /// Build valid new `Self` + pub fn build( + uri: Uri, + size: usize, + mime: String, + token: Option, + data: Vec, + ) -> Self { + Self { + uri, + size, + mime, + token, + data, + } // @TODO validate + } + + // Getters + + /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + pub fn to_bytes(&self) -> Bytes { + // Build header + let mut header = format!("{};size={};mime={}", self.uri, self.size, self.mime); + if let Some(ref token) = self.token { + header.push_str(&format!(";token={token}")); + } + header.push_str("\r\n"); + + let header_bytes = header.into_bytes(); + + // Build request + let mut bytes: Vec = Vec::with_capacity(self.size + header_bytes.len()); + bytes.extend(header_bytes); + bytes.extend(&self.data); + + // Wrap result + Bytes::from(&bytes) + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 1673a59..aa0aaaa 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1 +1,15 @@ +use gio::*; +use glib::*; + +use ggemini::client::connection::request::Gemini; + +#[test] +fn client_connection_request_gemini_build() { + const REQUEST: &str = "gemini://geminiprotocol.net/"; + + let request = Gemini::build(Uri::parse(REQUEST, UriFlags::NONE).unwrap()); + + assert_eq!(&request.uri.to_string(), REQUEST); +} + // @TODO From 54f2b81475506094a51ff9f2f71b05d582167074 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:23:52 +0200 Subject: [PATCH 247/392] add versions debug API --- src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 868a4f3..afd549d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,11 @@ pub use client::Client; // Global defaults pub const DEFAULT_PORT: u16 = 1965; + +// Debug + +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR"); +pub const VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR"); +pub const VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH"); From 74e5e4d97668857657daf61eca442b6bc63244a0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:25:05 +0200 Subject: [PATCH 248/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f49ce36..a45f447 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ fn main() -> ExitCode { } ``` -* to send requests using Titan protocol, see also `titan_request_async` implementation +* to send requests using Titan protocol, see `titan_request_async` implementation ## Other crates From fbdc20fd13d9b609291d4215463dbd9f2f7e047f Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:28:00 +0200 Subject: [PATCH 249/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a45f447..c9734be 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications with [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) dependency. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) and/or [gio](https://crates.io/crates/gio) (`2.66+`) bindings. ## Install From 007921f73f59299372c23b6ac8c898466c5d9259 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:30:19 +0200 Subject: [PATCH 250/392] add `libglib2.0-dev` dependency --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85edd8b..ad7ed4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,10 @@ jobs: - uses: actions/checkout@v4 - name: Run rustfmt run: cargo fmt --all -- --check + - name: Update packages index + run: sudo apt update + - name: Install system packages + run: sudo apt install -y libglib2.0-dev - name: Run clippy run: cargo clippy --all-targets - name: Build From fecbbff18fa0e28411263d93e32e4b188b519697 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:52:12 +0200 Subject: [PATCH 251/392] implement `to_network_address` method --- src/client.rs | 10 ++-------- src/client/connection/request.rs | 23 +++++++++++++++++++++++ src/client/connection/request/error.rs | 16 ++++++++++++++++ src/client/error.rs | 6 +----- 4 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 src/client/connection/request/error.rs diff --git a/src/client.rs b/src/client.rs index c92077a..ec54d51 100644 --- a/src/client.rs +++ b/src/client.rs @@ -65,13 +65,7 @@ impl Client { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) - match crate::gio::network_address::from_uri( - &match request { - Request::Gemini(ref request) => request.uri.clone(), - Request::Titan(ref request) => request.uri.clone(), - }, - crate::DEFAULT_PORT, - ) { + match request.to_network_address() { Ok(network_address) => { self.socket .connect_async(&network_address.clone(), Some(&cancellable.clone()), { @@ -112,7 +106,7 @@ impl Client { } }) } - Err(e) => callback(Err(Error::NetworkAddress(e))), + Err(e) => callback(Err(Error::Request(e))), } } diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 5986f7b..71ec63c 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -1,10 +1,33 @@ +pub mod error; pub mod gemini; pub mod titan; +pub use error::Error; pub use gemini::Gemini; pub use titan::Titan; +use gio::NetworkAddress; + +/// Single `Request` holder for different protocols pub enum Request { Gemini(Gemini), Titan(Titan), } + +impl Request { + // Getters + + /// Convert `Self` to [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) + pub fn to_network_address(&self) -> Result { + match crate::gio::network_address::from_uri( + &match self { + Request::Gemini(ref request) => request.uri.clone(), + Request::Titan(ref request) => request.uri.clone(), + }, + crate::DEFAULT_PORT, + ) { + Ok(network_address) => Ok(network_address), + Err(e) => Err(Error::NetworkAddress(e)), + } + } +} diff --git a/src/client/connection/request/error.rs b/src/client/connection/request/error.rs new file mode 100644 index 0000000..5524960 --- /dev/null +++ b/src/client/connection/request/error.rs @@ -0,0 +1,16 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + NetworkAddress(crate::gio::network_address::error::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::NetworkAddress(e) => { + write!(f, "Network Address error: {e}") + } + } + } +} diff --git a/src/client/error.rs b/src/client/error.rs index 0ebb3c3..6083e77 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -4,8 +4,7 @@ use std::fmt::{Display, Formatter, Result}; pub enum Error { Connect(glib::Error), Connection(crate::client::connection::Error), - NetworkAddress(crate::gio::network_address::Error), - Request(glib::Error), + Request(crate::client::connection::request::Error), } impl Display for Error { @@ -17,9 +16,6 @@ impl Display for Error { Self::Connect(e) => { write!(f, "Connect error: {e}") } - Self::NetworkAddress(e) => { - write!(f, "Network address error: {e}") - } Self::Request(e) => { write!(f, "Request error: {e}") } From 8492ea7db0002b7d90068176929034f1a9512936 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:54:55 +0200 Subject: [PATCH 252/392] update comment --- src/client/connection/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 71ec63c..4c822be 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -17,7 +17,7 @@ pub enum Request { impl Request { // Getters - /// Convert `Self` to [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) + /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self` pub fn to_network_address(&self) -> Result { match crate::gio::network_address::from_uri( &match self { From f93fd035ef290933b8fd06d9bf77ebf4805c0535 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:56:06 +0200 Subject: [PATCH 253/392] use `Self` alias --- src/client/connection/request.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 4c822be..59796e2 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -21,8 +21,8 @@ impl Request { pub fn to_network_address(&self) -> Result { match crate::gio::network_address::from_uri( &match self { - Request::Gemini(ref request) => request.uri.clone(), - Request::Titan(ref request) => request.uri.clone(), + Self::Gemini(ref request) => request.uri.clone(), + Self::Titan(ref request) => request.uri.clone(), }, crate::DEFAULT_PORT, ) { From a2261601f62a0fe32967405e9d647be81de91331 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 21:57:41 +0200 Subject: [PATCH 254/392] update comment --- src/client/connection/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 59796e2..544f86f 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -8,7 +8,7 @@ pub use titan::Titan; use gio::NetworkAddress; -/// Single `Request` holder for different protocols +/// Single `Request` implementation for different protocols pub enum Request { Gemini(Gemini), Titan(Titan), From fa02234cbd264a4778ab629b96cf0d3d93228003 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 22:05:53 +0200 Subject: [PATCH 255/392] implement `default_port` option for `to_network_address` method --- src/client.rs | 2 +- src/client/connection/request.rs | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index ec54d51..e94706c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -65,7 +65,7 @@ impl Client { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) - match request.to_network_address() { + match request.to_network_address(crate::DEFAULT_PORT) { Ok(network_address) => { self.socket .connect_async(&network_address.clone(), Some(&cancellable.clone()), { diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 544f86f..4fa1afe 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -18,13 +18,19 @@ impl Request { // Getters /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self` - pub fn to_network_address(&self) -> Result { + pub fn to_network_address(&self, default_port: u16) -> Result { + let uri = match self { + Self::Gemini(ref request) => request.uri.clone(), + Self::Titan(ref request) => request.uri.clone(), + }; + let port = uri.port(); match crate::gio::network_address::from_uri( - &match self { - Self::Gemini(ref request) => request.uri.clone(), - Self::Titan(ref request) => request.uri.clone(), + &uri, + if port.is_positive() { + port as u16 + } else { + default_port }, - crate::DEFAULT_PORT, ) { Ok(network_address) => Ok(network_address), Err(e) => Err(Error::NetworkAddress(e)), From 042e2a16ea757a48c5e6a1296e58af3a1d4b65bb Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 22:10:00 +0200 Subject: [PATCH 256/392] remove extra conditions --- src/client/connection/request.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 4fa1afe..acfa86f 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -23,15 +23,7 @@ impl Request { Self::Gemini(ref request) => request.uri.clone(), Self::Titan(ref request) => request.uri.clone(), }; - let port = uri.port(); - match crate::gio::network_address::from_uri( - &uri, - if port.is_positive() { - port as u16 - } else { - default_port - }, - ) { + match crate::gio::network_address::from_uri(&uri, default_port) { Ok(network_address) => Ok(network_address), Err(e) => Err(Error::NetworkAddress(e)), } From 1c0de40617b72a7eec5a50e9f0210c5fa633a98b Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 22:10:35 +0200 Subject: [PATCH 257/392] remove extra variable --- src/client/connection/request.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index acfa86f..adbefe0 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -19,11 +19,13 @@ impl Request { /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self` pub fn to_network_address(&self, default_port: u16) -> Result { - let uri = match self { - Self::Gemini(ref request) => request.uri.clone(), - Self::Titan(ref request) => request.uri.clone(), - }; - match crate::gio::network_address::from_uri(&uri, default_port) { + match crate::gio::network_address::from_uri( + &match self { + Self::Gemini(ref request) => request.uri.clone(), + Self::Titan(ref request) => request.uri.clone(), + }, + default_port, + ) { Ok(network_address) => Ok(network_address), Err(e) => Err(Error::NetworkAddress(e)), } From eab1786918f074b422963094e5762a8379976bfc Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 22:17:01 +0200 Subject: [PATCH 258/392] remove extra `size` argument --- src/client/connection/request/titan.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index 0affd3f..9f9dfd3 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -3,7 +3,6 @@ use glib::{Bytes, Uri}; /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol enum object for `Request` pub struct Titan { pub uri: Uri, - pub size: usize, pub mime: String, pub token: Option, pub data: Vec, @@ -13,16 +12,9 @@ impl Titan { // Constructors /// Build valid new `Self` - pub fn build( - uri: Uri, - size: usize, - mime: String, - token: Option, - data: Vec, - ) -> Self { + pub fn build(uri: Uri, mime: String, token: Option, data: Vec) -> Self { Self { uri, - size, mime, token, data, @@ -33,18 +25,19 @@ impl Titan { /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) pub fn to_bytes(&self) -> Bytes { + // Calculate data size + let size = self.data.len(); + // Build header - let mut header = format!("{};size={};mime={}", self.uri, self.size, self.mime); + let mut header = format!("{};size={size};mime={}", self.uri, self.mime); if let Some(ref token) = self.token { header.push_str(&format!(";token={token}")); } header.push_str("\r\n"); - let header_bytes = header.into_bytes(); - // Build request - let mut bytes: Vec = Vec::with_capacity(self.size + header_bytes.len()); - bytes.extend(header_bytes); + let mut bytes: Vec = Vec::with_capacity(size + 1024); // @TODO + bytes.extend(header.into_bytes()); bytes.extend(&self.data); // Wrap result From e2097138a98be7882d8235ee3b193bc9959b360a Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 22:46:43 +0200 Subject: [PATCH 259/392] add requirements installation guide --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index c9734be..fbfe119 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,20 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) and/or [gio](https://crates.io/crates/gio) (`2.66+`) bindings. +## Requirements + +
+Debian +
+sudo apt install libglib2.0-dev
+
+ +
+Fedora +
+sudo dnf install glib2-devel
+
+ ## Install ``` From ce5d3ac4d213b27f2a3917c1a05af68aa663a5a5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 23:14:54 +0200 Subject: [PATCH 260/392] implement `Request` constructors, remove `build` methods --- README.md | 6 ++---- src/client/connection/request.rs | 18 ++++++++++++++++++ src/client/connection/request/gemini.rs | 7 ------- src/client/connection/request/titan.rs | 12 ------------ tests/integration.rs | 15 +++++++++------ 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index fbfe119..6ea0832 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,8 @@ use ggemini::client::{ fn main() -> ExitCode { Client::new().request_async( - Request::Gemini( - Gemini::build( - Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap() - ) + Request::gemini( + Uri::parse(REQUEST, UriFlags::NONE).unwrap(), ), Priority::DEFAULT, Cancellable::new(), diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index adbefe0..df64d16 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -7,6 +7,7 @@ pub use gemini::Gemini; pub use titan::Titan; use gio::NetworkAddress; +use glib::Uri; /// Single `Request` implementation for different protocols pub enum Request { @@ -15,6 +16,23 @@ pub enum Request { } impl Request { + // Constructors + + /// Create new `Self` for [Gemini protocol](https://geminiprotocol.net) + pub fn gemini(uri: Uri) -> Self { + Self::Gemini(Gemini { uri }) + } + + /// Create new `Self` for [Titan protocol](gemini://transjovian.org/titan/page/The%20Titan%20Specification) + pub fn titan(uri: Uri, mime: String, token: Option, data: Vec) -> Self { + Self::Titan(Titan { + uri, + mime, + token, + data, + }) + } + // Getters /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self` diff --git a/src/client/connection/request/gemini.rs b/src/client/connection/request/gemini.rs index 4887499..78ec59b 100644 --- a/src/client/connection/request/gemini.rs +++ b/src/client/connection/request/gemini.rs @@ -6,13 +6,6 @@ pub struct Gemini { } impl Gemini { - // Constructors - - /// Build valid new `Self` - pub fn build(uri: Uri) -> Self { - Self { uri } // @TODO validate - } - // Getters /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index 9f9dfd3..6d1c781 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -9,18 +9,6 @@ pub struct Titan { } impl Titan { - // Constructors - - /// Build valid new `Self` - pub fn build(uri: Uri, mime: String, token: Option, data: Vec) -> Self { - Self { - uri, - mime, - token, - data, - } // @TODO validate - } - // Getters /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) diff --git a/tests/integration.rs b/tests/integration.rs index aa0aaaa..7755a6a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,15 +1,18 @@ use gio::*; use glib::*; -use ggemini::client::connection::request::Gemini; +use ggemini::client::connection::Request; #[test] -fn client_connection_request_gemini_build() { +fn client_connection_request_gemini() { const REQUEST: &str = "gemini://geminiprotocol.net/"; - - let request = Gemini::build(Uri::parse(REQUEST, UriFlags::NONE).unwrap()); - - assert_eq!(&request.uri.to_string(), REQUEST); + assert_eq!( + &match Request::gemini(Uri::parse(REQUEST, UriFlags::NONE).unwrap()) { + Request::Gemini(request) => request.uri.to_string(), + Request::Titan(_) => panic!(), + }, + REQUEST + ); } // @TODO From f4c9b73925bb78aab469afe7a9e38ac66980318f Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 23:41:12 +0200 Subject: [PATCH 261/392] make MIME type argument optional --- src/client/connection/request.rs | 4 ++-- src/client/connection/request/titan.rs | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index df64d16..04f58ae 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -24,12 +24,12 @@ impl Request { } /// Create new `Self` for [Titan protocol](gemini://transjovian.org/titan/page/The%20Titan%20Specification) - pub fn titan(uri: Uri, mime: String, token: Option, data: Vec) -> Self { + pub fn titan(uri: Uri, data: Vec, mime: Option, token: Option) -> Self { Self::Titan(Titan { uri, + data, mime, token, - data, }) } diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index 6d1c781..19b07e6 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -3,9 +3,9 @@ use glib::{Bytes, Uri}; /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol enum object for `Request` pub struct Titan { pub uri: Uri, - pub mime: String, - pub token: Option, pub data: Vec, + pub mime: Option, + pub token: Option, } impl Titan { @@ -17,7 +17,10 @@ impl Titan { let size = self.data.len(); // Build header - let mut header = format!("{};size={size};mime={}", self.uri, self.mime); + let mut header = format!("{};size={size}", self.uri); + if let Some(ref mime) = self.mime { + header.push_str(&format!(";mime={mime}")); + } if let Some(ref token) = self.token { header.push_str(&format!(";token={token}")); } From a9283770db21c9f337066dec571311c911873df5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 14 Jan 2025 00:42:17 +0200 Subject: [PATCH 262/392] implement `to_bytes` method --- src/client/connection/request.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 04f58ae..f1d8506 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -7,7 +7,7 @@ pub use gemini::Gemini; pub use titan::Titan; use gio::NetworkAddress; -use glib::Uri; +use glib::{Bytes, Uri}; /// Single `Request` implementation for different protocols pub enum Request { @@ -35,6 +35,14 @@ impl Request { // Getters + /// Get [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) for `Self` + pub fn to_bytes(&self) -> Bytes { + match self { + Self::Gemini(ref request) => request.to_bytes(), + Self::Titan(ref request) => request.to_bytes(), + } + } + /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self` pub fn to_network_address(&self, default_port: u16) -> Result { match crate::gio::network_address::from_uri( From df191c8e25f2d320c05229dc5872b4a20fd6baf5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 14 Jan 2025 00:42:40 +0200 Subject: [PATCH 263/392] add query support --- src/client/connection/request/titan.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index 19b07e6..504c8cb 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -1,4 +1,4 @@ -use glib::{Bytes, Uri}; +use glib::{Bytes, Uri, UriHideFlags}; /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol enum object for `Request` pub struct Titan { @@ -17,13 +17,19 @@ impl Titan { let size = self.data.len(); // Build header - let mut header = format!("{};size={size}", self.uri); + let mut header = format!( + "{};size={size}", + self.uri.to_string_partial(UriHideFlags::QUERY) + ); if let Some(ref mime) = self.mime { header.push_str(&format!(";mime={mime}")); } if let Some(ref token) = self.token { header.push_str(&format!(";token={token}")); } + if let Some(query) = self.uri.query() { + header.push_str(&format!("?{query}")); + } header.push_str("\r\n"); // Build request From ac53f73a603598a6aa2732007a671014d2739d38 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 14 Jan 2025 00:42:59 +0200 Subject: [PATCH 264/392] implement `titan` test --- tests/integration.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/integration.rs b/tests/integration.rs index 7755a6a..2d92c08 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -9,10 +9,36 @@ fn client_connection_request_gemini() { assert_eq!( &match Request::gemini(Uri::parse(REQUEST, UriFlags::NONE).unwrap()) { Request::Gemini(request) => request.uri.to_string(), - Request::Titan(_) => panic!(), + _ => panic!(), }, REQUEST ); } +#[test] +fn client_connection_request_titan() { + const DATA: &[u8] = &[1, 2, 3]; + const MIME: &str = "plain/text"; + const TOKEN: &str = "token"; + const ARGUMENT: &str = "argument"; + const REQUEST: &str = "titan://geminiprotocol.net/raw/Test"; + assert_eq!( + std::str::from_utf8( + &Request::titan( + Uri::parse(&format!("{REQUEST}?arg={ARGUMENT}"), UriFlags::NONE).unwrap(), + DATA.to_vec(), + Some(MIME.to_string()), + Some(TOKEN.to_string()), + ) + .to_bytes() + ) + .unwrap(), + format!( + "{REQUEST};size={};mime={MIME};token={TOKEN}?arg={ARGUMENT}\r\n{}", + DATA.len(), + std::str::from_utf8(DATA).unwrap(), + ) + ); +} + // @TODO From 339f0bb1af31b6e9dcf26b942bb50211203eaaaa Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 14 Jan 2025 00:44:12 +0200 Subject: [PATCH 265/392] change arg attribute name --- tests/integration.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index 2d92c08..98cf2f1 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -25,7 +25,7 @@ fn client_connection_request_titan() { assert_eq!( std::str::from_utf8( &Request::titan( - Uri::parse(&format!("{REQUEST}?arg={ARGUMENT}"), UriFlags::NONE).unwrap(), + Uri::parse(&format!("{REQUEST}?argument={ARGUMENT}"), UriFlags::NONE).unwrap(), DATA.to_vec(), Some(MIME.to_string()), Some(TOKEN.to_string()), @@ -34,7 +34,7 @@ fn client_connection_request_titan() { ) .unwrap(), format!( - "{REQUEST};size={};mime={MIME};token={TOKEN}?arg={ARGUMENT}\r\n{}", + "{REQUEST};size={};mime={MIME};token={TOKEN}?argument={ARGUMENT}\r\n{}", DATA.len(), std::str::from_utf8(DATA).unwrap(), ) From c67593e5fd4d337aff4d0102c844d9fad4ec6684 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 14 Jan 2025 00:47:53 +0200 Subject: [PATCH 266/392] update `gemini` test conditions --- tests/integration.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index 98cf2f1..0b38c7a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -7,11 +7,11 @@ use ggemini::client::connection::Request; fn client_connection_request_gemini() { const REQUEST: &str = "gemini://geminiprotocol.net/"; assert_eq!( - &match Request::gemini(Uri::parse(REQUEST, UriFlags::NONE).unwrap()) { - Request::Gemini(request) => request.uri.to_string(), - _ => panic!(), - }, - REQUEST + std::str::from_utf8( + &Request::gemini(Uri::parse(REQUEST, UriFlags::NONE).unwrap()).to_bytes() + ) + .unwrap(), + format!("{REQUEST}\r\n") ); } From 61fbab6dae9770cbc69e2412a1e6566a0bae4921 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 17 Jan 2025 07:32:09 +0200 Subject: [PATCH 267/392] use `FnOnce` for callback function --- src/client.rs | 2 +- src/client/connection.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index e94706c..206ac0d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -60,7 +60,7 @@ impl Client { priority: Priority, cancellable: Cancellable, certificate: Option, - callback: impl Fn(Result) + 'static, + callback: impl FnOnce(Result) + 'static, ) { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid diff --git a/src/client/connection.rs b/src/client/connection.rs index 380a115..66a745c 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -56,7 +56,7 @@ impl Connection { request: Request, priority: Priority, cancellable: Cancellable, - callback: impl Fn(Result) + 'static, + callback: impl FnOnce(Result) + 'static, ) { match request { Request::Gemini(request) => { @@ -77,7 +77,7 @@ impl Connection { request: Gemini, priority: Priority, cancellable: Cancellable, - callback: impl Fn(Result) + 'static, + callback: impl FnOnce(Result) + 'static, ) { self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); } @@ -91,7 +91,7 @@ impl Connection { request: Titan, priority: Priority, cancellable: Cancellable, - callback: impl Fn(Result) + 'static, + callback: impl FnOnce(Result) + 'static, ) { self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); } @@ -107,7 +107,7 @@ impl Connection { request: &Bytes, priority: Priority, cancellable: Cancellable, - callback: impl Fn(Result) + 'static, + callback: impl FnOnce(Result) + 'static, ) { self.stream().output_stream().write_bytes_async( request, From 09b2c626a3584ef68a5010a1c083274eb8a75f22 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 17 Jan 2025 22:26:19 +0200 Subject: [PATCH 268/392] implement `Display` trait --- src/client/connection/response/meta/status.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/client/connection/response/meta/status.rs b/src/client/connection/response/meta/status.rs index c0079f4..8f688c6 100644 --- a/src/client/connection/response/meta/status.rs +++ b/src/client/connection/response/meta/status.rs @@ -35,6 +35,35 @@ pub enum Status { 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 /// From d3133f50f72e65f6b30401ef3ead7435b5ff8ce9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:08:19 +0200 Subject: [PATCH 269/392] change `Mime` struct, implement `Display` trait --- src/client/connection/response/meta/mime.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 6378874..6e79bd9 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -6,11 +6,17 @@ pub use error::Error; use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters -pub struct Mime { - pub value: String, +pub struct Mime(String); + +impl std::fmt::Display for Mime { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } } 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, Error> { @@ -37,10 +43,17 @@ impl Mime { return Ok(None); } match parse(subject) { - Some(value) => Ok(Some(Self { value })), + Some(value) => Ok(Some(Self(value))), None => Err(Error::Undefined), } } + + // Getters + + /// Get `Self` as `&str` + pub fn as_str(&self) -> &str { + &self.0 + } } /// Extract MIME type from from string that includes **header** From e456719e58abfc7f3cb80c90873be8e7e4214618 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:18:56 +0200 Subject: [PATCH 270/392] store `MIME` value in lowercase presentation --- src/client/connection/response/meta/mime.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 6e79bd9..82fee21 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -29,7 +29,7 @@ impl Mime { // 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()), + Ok(string) => Self::from_string(&string), Err(e) => Err(Error::Decode(e)), }, None => Err(Error::Protocol), @@ -43,16 +43,16 @@ impl Mime { return Ok(None); } match parse(subject) { - Some(value) => Ok(Some(Self(value))), + Some(value) => Ok(Some(Self(value.to_lowercase()))), None => Err(Error::Undefined), } } // Getters - /// Get `Self` as `&str` + /// Get `Self` as lowercase `std::str` pub fn as_str(&self) -> &str { - &self.0 + self.0.as_str() } } From b5e864e807a68295ac428b0113f76ef129513986 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:27:23 +0200 Subject: [PATCH 271/392] update comment --- src/client/connection/response/meta/mime.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 82fee21..9593e9c 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -5,7 +5,8 @@ pub use error::Error; use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; -/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters +/// 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 std::fmt::Display for Mime { From c5aada49b4ee396c68a302e589a50307dcf16f79 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:36:08 +0200 Subject: [PATCH 272/392] remove extra conversions --- src/client/connection/response/meta/mime.rs | 20 +++++++++---------- .../connection/response/meta/mime/error.rs | 8 +++----- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 9593e9c..2b15d32 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -3,7 +3,7 @@ pub mod error; pub use error::Error; -use glib::{GString, Regex, RegexCompileFlags, RegexMatchFlags}; +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 @@ -29,22 +29,22 @@ impl Mime { // 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), + Some(utf8) => match std::str::from_utf8(utf8) { + Ok(s) => Self::from_string(s), Err(e) => Err(Error::Decode(e)), }, None => Err(Error::Protocol), } } - /// Create new `Self` from string that includes **header** + /// 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(subject: &str) -> Result, Error> { - if !subject.starts_with("2") { + pub fn from_string(s: &str) -> Result, Error> { + if !s.starts_with("2") { return Ok(None); } - match parse(subject) { - Some(value) => Ok(Some(Self(value.to_lowercase()))), + match parse(s) { + Some(v) => Ok(Some(Self(v.to_lowercase()))), None => Err(Error::Undefined), } } @@ -58,10 +58,10 @@ impl Mime { } /// Extract MIME type from from string that includes **header** -pub fn parse(value: &str) -> Option { +pub fn parse(s: &str) -> Option { Regex::split_simple( r"^2\d{1}\s([^\/]+\/[^\s;]+)", - value, + s, RegexCompileFlags::DEFAULT, RegexMatchFlags::DEFAULT, ) diff --git a/src/client/connection/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs index 9ea72ba..5b68dbc 100644 --- a/src/client/connection/response/meta/mime/error.rs +++ b/src/client/connection/response/meta/mime/error.rs @@ -1,14 +1,12 @@ -use std::fmt::{Display, Formatter, Result}; - #[derive(Debug)] pub enum Error { - Decode(std::string::FromUtf8Error), + Decode(std::str::Utf8Error), Protocol, Undefined, } -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { +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}") From edf9982933f94f46f009e1c44de144fd9b52d437 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:38:32 +0200 Subject: [PATCH 273/392] remove extra conversion --- src/client/connection/response/meta/mime.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 2b15d32..9ce4739 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -44,7 +44,7 @@ impl Mime { return Ok(None); } match parse(s) { - Some(v) => Ok(Some(Self(v.to_lowercase()))), + Some(v) => Ok(Some(Self(v))), None => Err(Error::Undefined), } } @@ -66,5 +66,5 @@ pub fn parse(s: &str) -> Option { RegexMatchFlags::DEFAULT, ) .get(1) - .map(|this| this.to_string()) + .map(|this| this.to_lowercase()) } From 962558c1238d720e58edec8f1969bfe04bc0381a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:40:21 +0200 Subject: [PATCH 274/392] reorder implementations --- src/client/connection/response/meta/mime.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 9ce4739..8e813eb 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -9,12 +9,6 @@ use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; /// * the value stored in lowercase pub struct Mime(String); -impl std::fmt::Display for Mime { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - impl Mime { // Constructors @@ -57,6 +51,12 @@ impl Mime { } } +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 { Regex::split_simple( From 5dd78dd43df30d7d622f564f90eafaa622725966 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:51:06 +0200 Subject: [PATCH 275/392] change `Data` struct, replace `GString` with `String`, implement `Display` trait --- src/client/connection/response/meta/data.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/client/connection/response/meta/data.rs b/src/client/connection/response/meta/data.rs index e86af61..00ea4f5 100644 --- a/src/client/connection/response/meta/data.rs +++ b/src/client/connection/response/meta/data.rs @@ -4,16 +4,12 @@ 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, -} +pub struct Data(String); impl Data { // Constructors @@ -47,9 +43,9 @@ impl Data { } // Assumes the bytes are valid UTF-8 - match GString::from_utf8(bytes) { - Ok(value) => Ok(match value.is_empty() { - false => Some(Self { value }), + match String::from_utf8(bytes) { + Ok(data) => Ok(match data.is_empty() { + false => Some(Self(data)), true => None, }), Err(e) => Err(Error::Decode(e)), @@ -59,3 +55,9 @@ impl Data { } } } + +impl std::fmt::Display for Data { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} From b5be9dcc76e24e2493ea2f0d31fb28fdf54c967a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:53:33 +0200 Subject: [PATCH 276/392] remove extra conversions --- src/client/connection/response/meta/status.rs | 6 ++---- src/client/connection/response/meta/status/error.rs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/client/connection/response/meta/status.rs b/src/client/connection/response/meta/status.rs index 8f688c6..6f11ee4 100644 --- a/src/client/connection/response/meta/status.rs +++ b/src/client/connection/response/meta/status.rs @@ -4,8 +4,6 @@ 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 { @@ -70,8 +68,8 @@ impl Status { /// * includes `Self::from_string` parser, it means that given buffer should contain some **header** pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.get(0..2) { - Some(value) => match GString::from_utf8(value.to_vec()) { - Ok(string) => Self::from_string(string.as_str()), + Some(value) => match std::str::from_utf8(value) { + Ok(s) => Self::from_string(s), Err(e) => Err(Error::Decode(e)), }, None => Err(Error::Protocol), diff --git a/src/client/connection/response/meta/status/error.rs b/src/client/connection/response/meta/status/error.rs index b535376..24090dd 100644 --- a/src/client/connection/response/meta/status/error.rs +++ b/src/client/connection/response/meta/status/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Decode(std::string::FromUtf8Error), + Decode(std::str::Utf8Error), Protocol, Undefined, } From d999e64e02d65eea49a1739f4a59f88f1abdfb6b Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:55:06 +0200 Subject: [PATCH 277/392] rename variable --- src/client/connection/response/meta/status.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection/response/meta/status.rs b/src/client/connection/response/meta/status.rs index 6f11ee4..0cd6861 100644 --- a/src/client/connection/response/meta/status.rs +++ b/src/client/connection/response/meta/status.rs @@ -68,7 +68,7 @@ impl Status { /// * includes `Self::from_string` parser, it means that given buffer should contain some **header** pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.get(0..2) { - Some(value) => match std::str::from_utf8(value) { + Some(b) => match std::str::from_utf8(b) { Ok(s) => Self::from_string(s), Err(e) => Err(Error::Decode(e)), }, From 90bff092691f56bcf90303e880ad3aba6a6f5e60 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 02:55:49 +0200 Subject: [PATCH 278/392] rename variable --- src/client/connection/response/meta/mime.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs index 8e813eb..12b6810 100644 --- a/src/client/connection/response/meta/mime.rs +++ b/src/client/connection/response/meta/mime.rs @@ -23,7 +23,7 @@ impl Mime { // Parse meta bytes only match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { - Some(utf8) => match std::str::from_utf8(utf8) { + Some(b) => match std::str::from_utf8(b) { Ok(s) => Self::from_string(s), Err(e) => Err(Error::Decode(e)), }, From cac544043ec46fe64ef64fe771ff9cf9715045cf Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 03:26:11 +0200 Subject: [PATCH 279/392] add `as_str` getter --- src/client/connection/response/meta/data.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client/connection/response/meta/data.rs b/src/client/connection/response/meta/data.rs index 00ea4f5..3642847 100644 --- a/src/client/connection/response/meta/data.rs +++ b/src/client/connection/response/meta/data.rs @@ -54,6 +54,13 @@ impl Data { None => Err(Error::Protocol), } } + + // Getters + + /// Get `Self` as `std::str` + pub fn as_str(&self) -> &str { + self.0.as_str() + } } impl std::fmt::Display for Data { From 946ff485be45eda2fc5d3d0a659249f8203c489b Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 18 Jan 2025 03:37:26 +0200 Subject: [PATCH 280/392] store value as `GString`, implement `as_gstring`, `to_gstring` methods --- src/client/connection/response/meta/data.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/meta/data.rs b/src/client/connection/response/meta/data.rs index 3642847..d5eef6b 100644 --- a/src/client/connection/response/meta/data.rs +++ b/src/client/connection/response/meta/data.rs @@ -4,12 +4,14 @@ 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(String); +pub struct Data(GString); impl Data { // Constructors @@ -43,7 +45,7 @@ impl Data { } // Assumes the bytes are valid UTF-8 - match String::from_utf8(bytes) { + match GString::from_utf8(bytes) { Ok(data) => Ok(match data.is_empty() { false => Some(Self(data)), true => None, @@ -61,6 +63,16 @@ impl Data { 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 { From 48c76767881c560a9ec20d60aecf2c620f3934af Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Jan 2025 03:29:22 +0200 Subject: [PATCH 281/392] change `Text` structure, implement `Display` trait --- src/client/connection/response/data/text.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/client/connection/response/data/text.rs b/src/client/connection/response/data/text.rs index 2d3cf2c..ea7c07c 100644 --- a/src/client/connection/response/data/text.rs +++ b/src/client/connection/response/data/text.rs @@ -15,9 +15,7 @@ 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, -} +pub struct Text(GString); impl Default for Text { fn default() -> Self { @@ -30,14 +28,12 @@ impl Text { /// Create new `Self` pub fn new() -> Self { - Self { - data: GString::new(), - } + Self(GString::new()) } /// Create new `Self` from string pub fn from_string(data: &str) -> Self { - Self { data: data.into() } + Self(data.into()) } /// Create new `Self` from UTF-8 buffer @@ -68,6 +64,12 @@ impl Text { } } +impl std::fmt::Display for Text { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + // Tools /// Asynchronously read all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) From e96771b926d7b80503325f6094b0ab74918cafe0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Jan 2025 03:52:46 +0200 Subject: [PATCH 282/392] add `as_gstring` method --- src/client/connection/response/data/text.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client/connection/response/data/text.rs b/src/client/connection/response/data/text.rs index ea7c07c..ecfcc15 100644 --- a/src/client/connection/response/data/text.rs +++ b/src/client/connection/response/data/text.rs @@ -62,6 +62,13 @@ impl Text { }, ); } + + // Getters + + /// Get `Self` as `glib::GString` + pub fn as_gstring(&self) -> &GString { + &self.0 + } } impl std::fmt::Display for Text { From 490a513ddf76077949cf1cfa777d7201e98bab3e Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Jan 2025 04:24:43 +0200 Subject: [PATCH 283/392] update member data type --- src/client/connection/response/data/text.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/client/connection/response/data/text.rs b/src/client/connection/response/data/text.rs index ecfcc15..aaa8aa1 100644 --- a/src/client/connection/response/data/text.rs +++ b/src/client/connection/response/data/text.rs @@ -15,7 +15,7 @@ pub const BUFFER_CAPACITY: usize = 0x400; // 1024 pub const BUFFER_MAX_SIZE: usize = 0xfffff; // 1M /// Container for text-based response data -pub struct Text(GString); +pub struct Text(String); impl Default for Text { fn default() -> Self { @@ -28,7 +28,7 @@ impl Text { /// Create new `Self` pub fn new() -> Self { - Self(GString::new()) + Self(String::new()) } /// Create new `Self` from string @@ -62,13 +62,6 @@ impl Text { }, ); } - - // Getters - - /// Get `Self` as `glib::GString` - pub fn as_gstring(&self) -> &GString { - &self.0 - } } impl std::fmt::Display for Text { From 52141f3dcab04eaffef7cecc67f015d4f545df2f Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 22 Jan 2025 10:13:15 +0200 Subject: [PATCH 284/392] remove extra functions --- src/client.rs | 30 +++++++++--------------------- src/client/connection.rs | 35 ----------------------------------- 2 files changed, 9 insertions(+), 56 deletions(-) diff --git a/src/client.rs b/src/client.rs index 206ac0d..3ae95f7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -78,27 +78,15 @@ impl Client { Some(network_address), is_session_resumption, ) { - Ok(connection) => match request { - Request::Gemini(request) => connection - .gemini_request_async( - request, - priority, - cancellable, - move |result| match result { - Ok(response) => callback(Ok(response)), - Err(e) => callback(Err(Error::Connection(e))), - }, - ), - Request::Titan(request) => connection.titan_request_async( - request, - priority, - cancellable, - move |result| match result { - Ok(response) => callback(Ok(response)), - Err(e) => callback(Err(Error::Connection(e))), - }, - ), - }, + Ok(connection) => connection.request_async( + request, + priority, + cancellable, + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), + }, + ), Err(e) => callback(Err(Error::Connection(e))), } } diff --git a/src/client/connection.rs b/src/client/connection.rs index 66a745c..fdda6dc 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -57,41 +57,6 @@ impl Connection { priority: Priority, cancellable: Cancellable, callback: impl FnOnce(Result) + 'static, - ) { - match request { - Request::Gemini(request) => { - self.gemini_request_async(request, priority, cancellable, callback) - } - Request::Titan(request) => { - self.titan_request_async(request, priority, cancellable, callback) - } - } - } - - /// Make new request to `Self` connection using - /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol - /// * callback with new `Response` on success or `Error` on failure - /// * see also `request_async` method to send multi-protocol requests - pub fn gemini_request_async( - self, - request: Gemini, - priority: Priority, - cancellable: Cancellable, - callback: impl FnOnce(Result) + 'static, - ) { - self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); - } - - /// Make new request to `Self` connection using - /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol - /// * callback with new `Response` on success or `Error` on failure - /// * see also `request_async` method to send multi-protocol requests - pub fn titan_request_async( - self, - request: Titan, - priority: Priority, - cancellable: Cancellable, - callback: impl FnOnce(Result) + 'static, ) { self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); } From 5e52e748702135a395de74ca3c8e5fe32bb2585d Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 22 Jan 2025 15:14:59 +0200 Subject: [PATCH 285/392] update request api --- src/client/connection.rs | 28 ++++++---------------- src/client/connection/error.rs | 6 ++--- src/client/connection/request.rs | 26 ++++---------------- src/client/connection/request/gemini.rs | 8 +++---- src/client/connection/request/titan.rs | 15 ++++-------- tests/integration.rs | 32 ++++++++++++------------- 6 files changed, 37 insertions(+), 78 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index fdda6dc..eae8d45 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -6,13 +6,15 @@ pub use error::Error; pub use request::{Gemini, Request, Titan}; pub use response::Response; +// Local dependencies + use gio::{ - prelude::{IOStreamExt, OutputStreamExt, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, }; use glib::{ object::{Cast, ObjectExt}, - Bytes, Priority, + Priority, }; pub struct Connection { @@ -58,24 +60,8 @@ impl Connection { cancellable: Cancellable, callback: impl FnOnce(Result) + 'static, ) { - self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); - } - - /// Low-level shared method to send raw bytes array over - /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or - /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol - /// * bytes array should include formatted header according to protocol selected - /// * for high-level requests see `gemini_request_async` and `titan_request_async` methods - /// * to construct multi-protocol request with single function, use `request_async` method - pub fn bytes_request_async( - self, - request: &Bytes, - priority: Priority, - cancellable: Cancellable, - callback: impl FnOnce(Result) + 'static, - ) { - self.stream().output_stream().write_bytes_async( - request, + self.stream().output_stream().write_async( + request.header().into_bytes(), priority, Some(&cancellable.clone()), move |result| match result { @@ -88,7 +74,7 @@ impl Connection { }) }) } - Err(e) => callback(Err(Error::Stream(e))), + Err((b, e)) => callback(Err(Error::Request((b, e)))), }, ); } diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 2948f0f..fe667e6 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,16 +2,16 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { + Request((Vec, glib::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::Request((_, e)) => { + write!(f, "Request error: {e}") } Self::Response(e) => { write!(f, "Response error: {e}") diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index f1d8506..790374e 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -7,7 +7,6 @@ pub use gemini::Gemini; pub use titan::Titan; use gio::NetworkAddress; -use glib::{Bytes, Uri}; /// Single `Request` implementation for different protocols pub enum Request { @@ -16,30 +15,13 @@ pub enum Request { } impl Request { - // Constructors - - /// Create new `Self` for [Gemini protocol](https://geminiprotocol.net) - pub fn gemini(uri: Uri) -> Self { - Self::Gemini(Gemini { uri }) - } - - /// Create new `Self` for [Titan protocol](gemini://transjovian.org/titan/page/The%20Titan%20Specification) - pub fn titan(uri: Uri, data: Vec, mime: Option, token: Option) -> Self { - Self::Titan(Titan { - uri, - data, - mime, - token, - }) - } - // Getters - /// Get [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) for `Self` - pub fn to_bytes(&self) -> Bytes { + /// Get header string for `Self` + pub fn header(&self) -> String { match self { - Self::Gemini(ref request) => request.to_bytes(), - Self::Titan(ref request) => request.to_bytes(), + Self::Gemini(ref this) => this.header(), + Self::Titan(ref this) => this.header(), } } diff --git a/src/client/connection/request/gemini.rs b/src/client/connection/request/gemini.rs index 78ec59b..322d58b 100644 --- a/src/client/connection/request/gemini.rs +++ b/src/client/connection/request/gemini.rs @@ -1,4 +1,4 @@ -use glib::{Bytes, Uri}; +use glib::Uri; /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol enum object for `Request` pub struct Gemini { @@ -8,8 +8,8 @@ pub struct Gemini { impl Gemini { // Getters - /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) - pub fn to_bytes(&self) -> Bytes { - Bytes::from(format!("{}\r\n", self.uri).as_bytes()) + /// Get header string for `Self` + pub fn header(&self) -> String { + format!("{}\r\n", self.uri) } } diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index 504c8cb..333a7ff 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -3,7 +3,7 @@ use glib::{Bytes, Uri, UriHideFlags}; /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol enum object for `Request` pub struct Titan { pub uri: Uri, - pub data: Vec, + pub data: Bytes, pub mime: Option, pub token: Option, } @@ -11,8 +11,8 @@ pub struct Titan { impl Titan { // Getters - /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) - pub fn to_bytes(&self) -> Bytes { + /// Get header string for `Self` + pub fn header(&self) -> String { // Calculate data size let size = self.data.len(); @@ -31,13 +31,6 @@ impl Titan { header.push_str(&format!("?{query}")); } header.push_str("\r\n"); - - // Build request - let mut bytes: Vec = Vec::with_capacity(size + 1024); // @TODO - bytes.extend(header.into_bytes()); - bytes.extend(&self.data); - - // Wrap result - Bytes::from(&bytes) + header } } diff --git a/tests/integration.rs b/tests/integration.rs index 0b38c7a..8205495 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -7,10 +7,10 @@ use ggemini::client::connection::Request; fn client_connection_request_gemini() { const REQUEST: &str = "gemini://geminiprotocol.net/"; assert_eq!( - std::str::from_utf8( - &Request::gemini(Uri::parse(REQUEST, UriFlags::NONE).unwrap()).to_bytes() - ) - .unwrap(), + Request::Gemini(ggemini::client::connection::Gemini { + uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() + }) + .header(), format!("{REQUEST}\r\n") ); } @@ -20,23 +20,21 @@ fn client_connection_request_titan() { const DATA: &[u8] = &[1, 2, 3]; const MIME: &str = "plain/text"; const TOKEN: &str = "token"; - const ARGUMENT: &str = "argument"; - const REQUEST: &str = "titan://geminiprotocol.net/raw/Test"; assert_eq!( - std::str::from_utf8( - &Request::titan( - Uri::parse(&format!("{REQUEST}?argument={ARGUMENT}"), UriFlags::NONE).unwrap(), - DATA.to_vec(), - Some(MIME.to_string()), - Some(TOKEN.to_string()), + Request::Titan(ggemini::client::connection::Titan { + uri: Uri::parse( + "titan://geminiprotocol.net/raw/Test?key=value", + UriFlags::NONE ) - .to_bytes() - ) - .unwrap(), + .unwrap(), + data: Bytes::from(DATA), + mime: Some(MIME.to_string()), + token: Some(TOKEN.to_string()) + }) + .header(), format!( - "{REQUEST};size={};mime={MIME};token={TOKEN}?argument={ARGUMENT}\r\n{}", + "titan://geminiprotocol.net/raw/Test;size={};mime={MIME};token={TOKEN}?key=value\r\n", DATA.len(), - std::str::from_utf8(DATA).unwrap(), ) ); } From 075b5605a06862f924975dfc9db1b09ebd76a715 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 22 Jan 2025 15:15:44 +0200 Subject: [PATCH 286/392] rename constructor --- src/client.rs | 2 +- src/client/connection.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3ae95f7..bcce693 100644 --- a/src/client.rs +++ b/src/client.rs @@ -72,7 +72,7 @@ impl Client { let is_session_resumption = self.is_session_resumption; move |result| match result { Ok(socket_connection) => { - match Connection::new( + match Connection::build( socket_connection, certificate, Some(network_address), diff --git a/src/client/connection.rs b/src/client/connection.rs index eae8d45..6c4ca3d 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -25,7 +25,7 @@ impl Connection { // Constructors /// Create new `Self` - pub fn new( + pub fn build( socket_connection: SocketConnection, certificate: Option, server_identity: Option, From ea9d7e4c5da5b918b632555c465427145ce5893d Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 22 Jan 2025 15:15:54 +0200 Subject: [PATCH 287/392] update example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ea0832..43f8419 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ use ggemini::client::{ fn main() -> ExitCode { Client::new().request_async( Request::gemini( - Uri::parse(REQUEST, UriFlags::NONE).unwrap(), + Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), ), Priority::DEFAULT, Cancellable::new(), From caa61bb808a45091efab08538ef5dc6ade7b53f4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 22 Jan 2025 16:27:28 +0200 Subject: [PATCH 288/392] implement `Uri` reference getter --- src/client/connection/request.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 790374e..e72753c 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -6,7 +6,10 @@ pub use error::Error; pub use gemini::Gemini; pub use titan::Titan; +// Local dependencies + use gio::NetworkAddress; +use glib::Uri; /// Single `Request` implementation for different protocols pub enum Request { @@ -17,7 +20,7 @@ pub enum Request { impl Request { // Getters - /// Get header string for `Self` + /// Generate header string for `Self` pub fn header(&self) -> String { match self { Self::Gemini(ref this) => this.header(), @@ -25,15 +28,17 @@ impl Request { } } + /// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html) + pub fn uri(&self) -> &Uri { + match self { + Self::Gemini(ref this) => &this.uri, + Self::Titan(ref this) => &this.uri, + } + } + /// Get [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) for `Self` pub fn to_network_address(&self, default_port: u16) -> Result { - match crate::gio::network_address::from_uri( - &match self { - Self::Gemini(ref request) => request.uri.clone(), - Self::Titan(ref request) => request.uri.clone(), - }, - default_port, - ) { + match crate::gio::network_address::from_uri(self.uri(), default_port) { Ok(network_address) => Ok(network_address), Err(e) => Err(Error::NetworkAddress(e)), } From ee0216a1a0fb54a74a175590ec9553140fa7f15a Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 22 Jan 2025 18:35:10 +0200 Subject: [PATCH 289/392] handle Titan requests --- src/client/connection.rs | 45 +++++++++++++++++++++++++--------- src/client/connection/error.rs | 4 +-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 6c4ca3d..842c430 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -9,7 +9,7 @@ pub use response::Response; // Local dependencies use gio::{ - prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExt, OutputStreamExtManual, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, }; use glib::{ @@ -60,23 +60,44 @@ impl Connection { cancellable: Cancellable, callback: impl FnOnce(Result) + 'static, ) { - self.stream().output_stream().write_async( + let output_stream = self.stream().output_stream(); + output_stream.clone().write_async( request.header().into_bytes(), priority, Some(&cancellable.clone()), move |result| match result { - Ok(_) => { - // Read response - Response::from_connection_async(self, priority, cancellable, move |result| { - callback(match result { - Ok(response) => Ok(response), - Err(e) => Err(Error::Response(e)), + Ok(_) => match request { + Request::Gemini(..) => { + Response::from_connection_async(self, priority, cancellable, |result| { + callback(match result { + Ok(response) => Ok(response), + Err(e) => Err(Error::Response(e)), + }) }) - }) - } - Err((b, e)) => callback(Err(Error::Request((b, e)))), + } + Request::Titan(this) => output_stream.write_bytes_async( + &this.data, + priority, + Some(&cancellable.clone()), + move |result| match result { + Ok(_) => Response::from_connection_async( + self, + priority, + cancellable, + |result| { + callback(match result { + Ok(response) => Ok(response), + Err(e) => Err(Error::Response(e)), + }) + }, + ), + Err(e) => callback(Err(Error::Request(e))), + }, + ), + }, + Err((_, e)) => callback(Err(Error::Request(e))), }, - ); + ) } // Getters diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index fe667e6..178cfba 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Request((Vec, glib::Error)), + Request(glib::Error), Response(crate::client::connection::response::Error), TlsClientConnection(glib::Error), } @@ -10,7 +10,7 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Request((_, e)) => { + Self::Request(e) => { write!(f, "Request error: {e}") } Self::Response(e) => { From 37d30d700cc1c1aa8f980e1b3db85d7ac51775a4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 25 Jan 2025 20:51:16 +0200 Subject: [PATCH 290/392] define size inline --- src/client/connection/request/titan.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index 333a7ff..c44b5f4 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -13,13 +13,10 @@ impl Titan { /// Get header string for `Self` pub fn header(&self) -> String { - // Calculate data size - let size = self.data.len(); - - // Build header let mut header = format!( - "{};size={size}", - self.uri.to_string_partial(UriHideFlags::QUERY) + "{};size={}", + self.uri.to_string_partial(UriHideFlags::QUERY), + self.data.len() ); if let Some(ref mime) = self.mime { header.push_str(&format!(";mime={mime}")); From 67989dba630f4d71a38cb03249ac1a9200233607 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 25 Jan 2025 21:27:42 +0200 Subject: [PATCH 291/392] implement local tests --- src/client/connection/request/gemini.rs | 15 +++++++++ src/client/connection/request/titan.rs | 28 +++++++++++++++++ tests/integration.rs | 42 ------------------------- 3 files changed, 43 insertions(+), 42 deletions(-) delete mode 100644 tests/integration.rs diff --git a/src/client/connection/request/gemini.rs b/src/client/connection/request/gemini.rs index 322d58b..0961713 100644 --- a/src/client/connection/request/gemini.rs +++ b/src/client/connection/request/gemini.rs @@ -13,3 +13,18 @@ impl Gemini { format!("{}\r\n", self.uri) } } + +#[test] +fn header() { + use super::{super::Request, Gemini}; + use glib::UriFlags; + + const REQUEST: &str = "gemini://geminiprotocol.net/"; + assert_eq!( + Request::Gemini(Gemini { + uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() + }) + .header(), + format!("{REQUEST}\r\n") + ); +} diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index c44b5f4..70b9376 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -31,3 +31,31 @@ impl Titan { header } } + +#[test] +fn header() { + use super::{super::Request, Titan}; + use glib::UriFlags; + + const DATA: &[u8] = &[1, 2, 3]; + const MIME: &str = "plain/text"; + const TOKEN: &str = "token"; + + assert_eq!( + Request::Titan(Titan { + uri: Uri::parse( + "titan://geminiprotocol.net/raw/path?key=value", + UriFlags::NONE + ) + .unwrap(), + data: Bytes::from(DATA), + mime: Some(MIME.to_string()), + token: Some(TOKEN.to_string()) + }) + .header(), + format!( + "titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n", + DATA.len(), + ) + ); +} diff --git a/tests/integration.rs b/tests/integration.rs deleted file mode 100644 index 8205495..0000000 --- a/tests/integration.rs +++ /dev/null @@ -1,42 +0,0 @@ -use gio::*; -use glib::*; - -use ggemini::client::connection::Request; - -#[test] -fn client_connection_request_gemini() { - const REQUEST: &str = "gemini://geminiprotocol.net/"; - assert_eq!( - Request::Gemini(ggemini::client::connection::Gemini { - uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() - }) - .header(), - format!("{REQUEST}\r\n") - ); -} - -#[test] -fn client_connection_request_titan() { - const DATA: &[u8] = &[1, 2, 3]; - const MIME: &str = "plain/text"; - const TOKEN: &str = "token"; - assert_eq!( - Request::Titan(ggemini::client::connection::Titan { - uri: Uri::parse( - "titan://geminiprotocol.net/raw/Test?key=value", - UriFlags::NONE - ) - .unwrap(), - data: Bytes::from(DATA), - mime: Some(MIME.to_string()), - token: Some(TOKEN.to_string()) - }) - .header(), - format!( - "titan://geminiprotocol.net/raw/Test;size={};mime={MIME};token={TOKEN}?key=value\r\n", - DATA.len(), - ) - ); -} - -// @TODO From aa44e2723d174456ad8a07f1df9ddc0f8fce73da Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 25 Jan 2025 21:29:41 +0200 Subject: [PATCH 292/392] update readme --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 43f8419..aa8e128 100644 --- a/README.md +++ b/README.md @@ -63,25 +63,23 @@ fn main() -> ExitCode { Ok(response) => { // route by status code match response.meta.status { - // is code 20, handle `GIOStream` by content type + // code 20, handle `GIOStream` by content type Status::Success => match response.meta.mime.unwrap().value.as_str() { - // is gemtext, see ggemtext crate to parse + // gemtext, see ggemtext crate to parse "text/gemini" => todo!(), - // other types + // other content types _ => todo!(), }, _ => todo!(), } } - Err(e) => todo!("{e}"), + Err(_) => todo!(), }, ); ExitCode::SUCCESS } ``` -* to send requests using Titan protocol, see `titan_request_async` implementation - ## Other crates * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file From 86af47ff49bb2541d30e5025d698ad3216ad4459 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 25 Jan 2025 22:16:47 +0200 Subject: [PATCH 293/392] add `DEFAULT_MIME` const with comments --- src/client/connection/request/titan.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index 70b9376..de181a2 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -1,9 +1,15 @@ use glib::{Bytes, Uri, UriHideFlags}; -/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol enum object for `Request` +/// Optionally use this value by default for the text input +pub const DEFAULT_MIME: &str = "text/gemini"; + +/// Formatted [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) `Request` pub struct Titan { pub uri: Uri, pub data: Bytes, + /// MIME type is optional argument by Titan protocol specification, + /// but server MAY reject the request without `mime` value provided + /// * see also `DEFAULT_MIME` pub mime: Option, pub token: Option, } From f669dc6b235bff1f5fa4d6fa5c2ab639c367c25a Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 25 Jan 2025 23:41:28 +0200 Subject: [PATCH 294/392] remove extras --- src/client/connection/request/titan.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs index de181a2..61d5c11 100644 --- a/src/client/connection/request/titan.rs +++ b/src/client/connection/request/titan.rs @@ -1,15 +1,11 @@ use glib::{Bytes, Uri, UriHideFlags}; -/// Optionally use this value by default for the text input -pub const DEFAULT_MIME: &str = "text/gemini"; - /// Formatted [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) `Request` pub struct Titan { pub uri: Uri, pub data: Bytes, - /// MIME type is optional argument by Titan protocol specification, - /// but server MAY reject the request without `mime` value provided - /// * see also `DEFAULT_MIME` + /// MIME type is optional attribute by Titan protocol specification, + /// but server MAY reject the request without `mime` value provided. pub mime: Option, pub token: Option, } From 0cb5ff9cbccfd6aa9cc815ff97ac69e9b4cfcec2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 26 Jan 2025 08:28:14 +0200 Subject: [PATCH 295/392] update example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa8e128..50524da 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ use ggemini::client::{ fn main() -> ExitCode { Client::new().request_async( - Request::gemini( + Request::gemini( // or `Request::titan` Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), ), Priority::DEFAULT, From 6da4c2ed5230757c505bf752da837c8305f7e578 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 27 Jan 2025 20:42:17 +0200 Subject: [PATCH 296/392] implement titan and gemini requests in single file --- README.md | 6 +- src/client/connection.rs | 8 +-- src/client/connection/request.rs | 92 +++++++++++++++++++++---- src/client/connection/request/gemini.rs | 30 -------- src/client/connection/request/titan.rs | 63 ----------------- 5 files changed, 87 insertions(+), 112 deletions(-) delete mode 100644 src/client/connection/request/gemini.rs delete mode 100644 src/client/connection/request/titan.rs diff --git a/README.md b/README.md index 50524da..3a84f77 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,9 @@ use ggemini::client::{ fn main() -> ExitCode { Client::new().request_async( - Request::gemini( // or `Request::titan` - Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), - ), + Request::Gemini { // or `Request::Titan` + uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), + }, Priority::DEFAULT, Cancellable::new(), None, // optional `GTlsCertificate` diff --git a/src/client/connection.rs b/src/client/connection.rs index 842c430..9af51ed 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -3,7 +3,7 @@ pub mod request; pub mod response; pub use error::Error; -pub use request::{Gemini, Request, Titan}; +pub use request::Request; pub use response::Response; // Local dependencies @@ -67,7 +67,7 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match request { - Request::Gemini(..) => { + Request::Gemini { .. } => { Response::from_connection_async(self, priority, cancellable, |result| { callback(match result { Ok(response) => Ok(response), @@ -75,8 +75,8 @@ impl Connection { }) }) } - Request::Titan(this) => output_stream.write_bytes_async( - &this.data, + Request::Titan { data, .. } => output_stream.write_bytes_async( + &data, priority, Some(&cancellable.clone()), move |result| match result { diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index e72753c..46576a3 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -1,20 +1,24 @@ pub mod error; -pub mod gemini; -pub mod titan; - pub use error::Error; -pub use gemini::Gemini; -pub use titan::Titan; // Local dependencies use gio::NetworkAddress; -use glib::Uri; +use glib::{Bytes, Uri, UriHideFlags}; /// Single `Request` implementation for different protocols pub enum Request { - Gemini(Gemini), - Titan(Titan), + Gemini { + uri: Uri, + }, + Titan { + uri: Uri, + data: Bytes, + /// MIME type is optional attribute by Titan protocol specification, + /// but server MAY reject the request without `mime` value provided. + mime: Option, + token: Option, + }, } impl Request { @@ -23,16 +27,38 @@ impl Request { /// Generate header string for `Self` pub fn header(&self) -> String { match self { - Self::Gemini(ref this) => this.header(), - Self::Titan(ref this) => this.header(), + Self::Gemini { uri } => format!("{uri}\r\n"), + Self::Titan { + uri, + data, + mime, + token, + } => { + let mut header = format!( + "{};size={}", + uri.to_string_partial(UriHideFlags::QUERY), + data.len() + ); + if let Some(ref mime) = mime { + header.push_str(&format!(";mime={mime}")); + } + if let Some(ref token) = token { + header.push_str(&format!(";token={token}")); + } + if let Some(query) = uri.query() { + header.push_str(&format!("?{query}")); + } + header.push_str("\r\n"); + header + } } } /// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html) pub fn uri(&self) -> &Uri { match self { - Self::Gemini(ref this) => &this.uri, - Self::Titan(ref this) => &this.uri, + Self::Gemini { uri } => uri, + Self::Titan { uri, .. } => uri, } } @@ -44,3 +70,45 @@ impl Request { } } } + +#[test] +fn test_gemini_header() { + use glib::UriFlags; + + const REQUEST: &str = "gemini://geminiprotocol.net/"; + + assert_eq!( + Request::Gemini { + uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() + } + .header(), + format!("{REQUEST}\r\n") + ); +} + +#[test] +fn test_titan_header() { + use glib::UriFlags; + + const DATA: &[u8] = &[1, 2, 3]; + const MIME: &str = "plain/text"; + const TOKEN: &str = "token"; + + assert_eq!( + Request::Titan { + uri: Uri::parse( + "titan://geminiprotocol.net/raw/path?key=value", + UriFlags::NONE + ) + .unwrap(), + data: Bytes::from(DATA), + mime: Some(MIME.to_string()), + token: Some(TOKEN.to_string()) + } + .header(), + format!( + "titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n", + DATA.len(), + ) + ); +} diff --git a/src/client/connection/request/gemini.rs b/src/client/connection/request/gemini.rs deleted file mode 100644 index 0961713..0000000 --- a/src/client/connection/request/gemini.rs +++ /dev/null @@ -1,30 +0,0 @@ -use glib::Uri; - -/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol enum object for `Request` -pub struct Gemini { - pub uri: Uri, -} - -impl Gemini { - // Getters - - /// Get header string for `Self` - pub fn header(&self) -> String { - format!("{}\r\n", self.uri) - } -} - -#[test] -fn header() { - use super::{super::Request, Gemini}; - use glib::UriFlags; - - const REQUEST: &str = "gemini://geminiprotocol.net/"; - assert_eq!( - Request::Gemini(Gemini { - uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() - }) - .header(), - format!("{REQUEST}\r\n") - ); -} diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs deleted file mode 100644 index 61d5c11..0000000 --- a/src/client/connection/request/titan.rs +++ /dev/null @@ -1,63 +0,0 @@ -use glib::{Bytes, Uri, UriHideFlags}; - -/// Formatted [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) `Request` -pub struct Titan { - pub uri: Uri, - pub data: Bytes, - /// MIME type is optional attribute by Titan protocol specification, - /// but server MAY reject the request without `mime` value provided. - pub mime: Option, - pub token: Option, -} - -impl Titan { - // Getters - - /// Get header string for `Self` - pub fn header(&self) -> String { - let mut header = format!( - "{};size={}", - self.uri.to_string_partial(UriHideFlags::QUERY), - self.data.len() - ); - if let Some(ref mime) = self.mime { - header.push_str(&format!(";mime={mime}")); - } - if let Some(ref token) = self.token { - header.push_str(&format!(";token={token}")); - } - if let Some(query) = self.uri.query() { - header.push_str(&format!("?{query}")); - } - header.push_str("\r\n"); - header - } -} - -#[test] -fn header() { - use super::{super::Request, Titan}; - use glib::UriFlags; - - const DATA: &[u8] = &[1, 2, 3]; - const MIME: &str = "plain/text"; - const TOKEN: &str = "token"; - - assert_eq!( - Request::Titan(Titan { - uri: Uri::parse( - "titan://geminiprotocol.net/raw/path?key=value", - UriFlags::NONE - ) - .unwrap(), - data: Bytes::from(DATA), - mime: Some(MIME.to_string()), - token: Some(TOKEN.to_string()) - }) - .header(), - format!( - "titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n", - DATA.len(), - ) - ); -} From 016f82d586fb7360addf838295ebb703c3cc89c5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 27 Jan 2025 21:09:02 +0200 Subject: [PATCH 297/392] remove deprecated namespace --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 3a84f77..91e7552 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,6 @@ use glib::*; use ggemini::client::{ connection::{ Request, Response, - request::Gemini, response::meta::{Mime, Status} }, Client, Error, From eee87d66b4011dbf58272ccc08568d3dbaa21b0a Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 28 Jan 2025 10:19:24 +0200 Subject: [PATCH 298/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b19da7b..52cb044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.14.0" +version = "0.14.1" edition = "2021" license = "MIT" readme = "README.md" From cdac038135294b1d6e2bf2ebad52241f264b8e0f Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 28 Jan 2025 11:58:35 +0200 Subject: [PATCH 299/392] fix example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91e7552..23ee9de 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ fn main() -> ExitCode { // route by status code match response.meta.status { // code 20, handle `GIOStream` by content type - Status::Success => match response.meta.mime.unwrap().value.as_str() { + Status::Success => match response.meta.mime.unwrap().as_str() { // gemtext, see ggemtext crate to parse "text/gemini" => todo!(), // other content types From 5358e43697f50e2b9062f00bb7aa2cf95d5373ac Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 2 Feb 2025 22:12:40 +0200 Subject: [PATCH 300/392] update `Response` API --- Cargo.toml | 2 +- src/client/connection/response.rs | 125 +++++++++++-- src/client/connection/response/certificate.rs | 113 ++++++++++++ .../connection/response/certificate/error.rs | 23 +++ src/client/connection/response/error.rs | 45 ++++- src/client/connection/response/failure.rs | 54 ++++++ .../connection/response/failure/error.rs | 28 +++ .../connection/response/failure/permanent.rs | 169 ++++++++++++++++++ .../response/failure/permanent/error.rs | 23 +++ .../connection/response/failure/temporary.rs | 169 ++++++++++++++++++ .../response/failure/temporary/error.rs | 23 +++ src/client/connection/response/input.rs | 109 +++++++++++ .../response/{meta/data => input}/error.rs | 11 +- src/client/connection/response/meta.rs | 147 --------------- .../connection/response/meta/charset.rs | 1 - src/client/connection/response/meta/data.rs | 82 --------- src/client/connection/response/meta/error.rs | 33 ---- .../connection/response/meta/language.rs | 1 - src/client/connection/response/meta/mime.rs | 70 -------- .../connection/response/meta/mime/error.rs | 22 --- src/client/connection/response/meta/status.rs | 109 ----------- src/client/connection/response/redirect.rs | 112 ++++++++++++ .../connection/response/redirect/error.rs | 27 +++ src/client/connection/response/success.rs | 89 +++++++++ .../{meta/status => success}/error.rs | 17 +- 25 files changed, 1102 insertions(+), 502 deletions(-) create mode 100644 src/client/connection/response/certificate.rs create mode 100644 src/client/connection/response/certificate/error.rs create mode 100644 src/client/connection/response/failure.rs create mode 100644 src/client/connection/response/failure/error.rs create mode 100644 src/client/connection/response/failure/permanent.rs create mode 100644 src/client/connection/response/failure/permanent/error.rs create mode 100644 src/client/connection/response/failure/temporary.rs create mode 100644 src/client/connection/response/failure/temporary/error.rs create mode 100644 src/client/connection/response/input.rs rename src/client/connection/response/{meta/data => input}/error.rs (59%) delete mode 100644 src/client/connection/response/meta.rs delete mode 100644 src/client/connection/response/meta/charset.rs delete mode 100644 src/client/connection/response/meta/data.rs delete mode 100644 src/client/connection/response/meta/error.rs delete mode 100644 src/client/connection/response/meta/language.rs delete mode 100644 src/client/connection/response/meta/mime.rs delete mode 100644 src/client/connection/response/meta/mime/error.rs delete mode 100644 src/client/connection/response/meta/status.rs create mode 100644 src/client/connection/response/redirect.rs create mode 100644 src/client/connection/response/redirect/error.rs create mode 100644 src/client/connection/response/success.rs rename src/client/connection/response/{meta/status => success}/error.rs (52%) diff --git a/Cargo.toml b/Cargo.toml index 52cb044..f54e4c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.14.1" +version = "0.15.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 3ede7d0..9f2b901 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -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) + 'static, ) { - Meta::from_stream_async(connection.stream(), priority, cancellable, |result| { - callback(match result { - Ok(meta) => Ok(Self { connection, meta }), - Err(e) => Err(Error::Meta(e)), - }) - }) + from_stream_async( + Vec::with_capacity(HEADER_LEN), + connection.stream(), + cancellable, + priority, + |result| { + callback(match result { + Ok(buffer) => match buffer.first() { + Some(byte) => match byte { + 1 => match Input::from_utf8(&buffer) { + Ok(input) => Ok(Self::Input(input)), + Err(e) => Err(Error::Input(e)), + }, + 2 => match Success::from_utf8(&buffer) { + Ok(success) => Ok(Self::Success(success)), + Err(e) => Err(Error::Success(e)), + }, + 3 => match Redirect::from_utf8(&buffer) { + Ok(redirect) => Ok(Self::Redirect(redirect)), + Err(e) => Err(Error::Redirect(e)), + }, + 4 | 5 => match Failure::from_utf8(&buffer) { + Ok(failure) => Ok(Self::Failure(failure)), + Err(e) => Err(Error::Failure(e)), + }, + 6 => match Certificate::from_utf8(&buffer) { + Ok(certificate) => Ok(Self::Certificate(certificate)), + Err(e) => Err(Error::Certificate(e)), + }, + b => Err(Error::Code(*b)), + }, + None => Err(Error::Protocol), + }, + Err(e) => Err(e), + }) + }, + ); } } + +// Tools + +/// Asynchronously read header bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// +/// Return UTF-8 buffer collected +/// * requires `IOStream` reference to keep `Connection` active in async thread +fn from_stream_async( + mut buffer: Vec, + stream: impl IsA, + cancellable: Cancellable, + priority: Priority, + on_complete: impl FnOnce(Result, 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))), + }, + ) +} diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs new file mode 100644 index 0000000..450d6d7 --- /dev/null +++ b/src/client/connection/response/certificate.rs @@ -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 }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized + NotAuthorized { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid + NotValid { message: Option }, +} + +impl Certificate { + // Constructors + + /// Create new `Self` from buffer include header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + 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 { + 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 { + 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); +} diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/certificate/error.rs @@ -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}") + } + } + } +} diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index 9834190..99895d9 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -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), + 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}") } } } diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs new file mode 100644 index 0000000..c0e76ec --- /dev/null +++ b/src/client/connection/response/failure.rs @@ -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 { + 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(), + } + } +} diff --git a/src/client/connection/response/failure/error.rs b/src/client/connection/response/failure/error.rs new file mode 100644 index 0000000..60724eb --- /dev/null +++ b/src/client/connection/response/failure/error.rs @@ -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}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs new file mode 100644 index 0000000..08b6c56 --- /dev/null +++ b/src/client/connection/response/failure/permanent.rs @@ -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 }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found + NotFound { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone + Gone { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused + ProxyRequestRefused { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request + BadRequest { message: Option }, +} + +impl Permanent { + // Constructors + + /// Create new `Self` from buffer include header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + 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 { + 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 { + 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); +} diff --git a/src/client/connection/response/failure/permanent/error.rs b/src/client/connection/response/failure/permanent/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/failure/permanent/error.rs @@ -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}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs new file mode 100644 index 0000000..363645e --- /dev/null +++ b/src/client/connection/response/failure/temporary.rs @@ -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 }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable + ServerUnavailable { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error + CgiError { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error + ProxyError { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down + SlowDown { message: Option }, +} + +impl Temporary { + // Constructors + + /// Create new `Self` from buffer include header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + 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 { + 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 { + 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); +} diff --git a/src/client/connection/response/failure/temporary/error.rs b/src/client/connection/response/failure/temporary/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/failure/temporary/error.rs @@ -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}") + } + } + } +} diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs new file mode 100644 index 0000000..56dcc08 --- /dev/null +++ b/src/client/connection/response/input.rs @@ -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 }, + Sensitive { message: Option }, +} + +impl Input { + // Constructors + + /// Create new `Self` from buffer include header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + 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 { + 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 { + 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); +} diff --git a/src/client/connection/response/meta/data/error.rs b/src/client/connection/response/input/error.rs similarity index 59% rename from src/client/connection/response/meta/data/error.rs rename to src/client/connection/response/input/error.rs index 49455cd..ae589e8 100644 --- a/src/client/connection/response/meta/data/error.rs +++ b/src/client/connection/response/input/error.rs @@ -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") diff --git a/src/client/connection/response/meta.rs b/src/client/connection/response/meta.rs deleted file mode 100644 index 26388ad..0000000 --- a/src/client/connection/response/meta.rs +++ /dev/null @@ -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, - pub mime: Option, - // @TODO - // charset: Option, - // language: Option, -} - -impl Meta { - // Constructors - - /// Create new `Self` from UTF-8 buffer - /// * supports entire response or just meta slice - pub fn from_utf8(buffer: &[u8]) -> Result { - // 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, - priority: Priority, - cancellable: Cancellable, - on_complete: impl FnOnce(Result) + '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, - stream: impl IsA, - cancellable: Cancellable, - priority: Priority, - on_complete: impl FnOnce(Result, 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))), - }, - ); -} diff --git a/src/client/connection/response/meta/charset.rs b/src/client/connection/response/meta/charset.rs deleted file mode 100644 index 1673a59..0000000 --- a/src/client/connection/response/meta/charset.rs +++ /dev/null @@ -1 +0,0 @@ -// @TODO diff --git a/src/client/connection/response/meta/data.rs b/src/client/connection/response/meta/data.rs deleted file mode 100644 index d5eef6b..0000000 --- a/src/client/connection/response/meta/data.rs +++ /dev/null @@ -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, Error> { - // Define max buffer length for this method - const MAX_LEN: usize = 0x400; // 1024 - - // Init bytes buffer - let mut bytes: Vec = 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) - } -} diff --git a/src/client/connection/response/meta/error.rs b/src/client/connection/response/meta/error.rs deleted file mode 100644 index 55abb24..0000000 --- a/src/client/connection/response/meta/error.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Data(super::data::Error), - InputStream(Vec, 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}") - } - } - } -} diff --git a/src/client/connection/response/meta/language.rs b/src/client/connection/response/meta/language.rs deleted file mode 100644 index 1673a59..0000000 --- a/src/client/connection/response/meta/language.rs +++ /dev/null @@ -1 +0,0 @@ -// @TODO diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs deleted file mode 100644 index 12b6810..0000000 --- a/src/client/connection/response/meta/mime.rs +++ /dev/null @@ -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, 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, 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 { - Regex::split_simple( - r"^2\d{1}\s([^\/]+\/[^\s;]+)", - s, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ) - .get(1) - .map(|this| this.to_lowercase()) -} diff --git a/src/client/connection/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs deleted file mode 100644 index 5b68dbc..0000000 --- a/src/client/connection/response/meta/mime/error.rs +++ /dev/null @@ -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") - } - } - } -} diff --git a/src/client/connection/response/meta/status.rs b/src/client/connection/response/meta/status.rs deleted file mode 100644 index 0cd6861..0000000 --- a/src/client/connection/response/meta/status.rs +++ /dev/null @@ -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 { - 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 { - 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), - } - } -} diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs new file mode 100644 index 0000000..cfb8c21 --- /dev/null +++ b/src/client/connection/response/redirect.rs @@ -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 { + 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 { + 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 { + 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); +} diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs new file mode 100644 index 0000000..b3d3579 --- /dev/null +++ b/src/client/connection/response/redirect/error.rs @@ -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") + } + } + } +} diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs new file mode 100644 index 0000000..e5ad6f4 --- /dev/null +++ b/src/client/connection/response/success.rs @@ -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 { + 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 { + 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); +} diff --git a/src/client/connection/response/meta/status/error.rs b/src/client/connection/response/success/error.rs similarity index 52% rename from src/client/connection/response/meta/status/error.rs rename to src/client/connection/response/success/error.rs index 24090dd..2dbe363 100644 --- a/src/client/connection/response/meta/status/error.rs +++ b/src/client/connection/response/success/error.rs @@ -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") } } } From 8df7af44b59e2f2cb44e315bc73ed19ae1cbb7d9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 2 Feb 2025 23:08:42 +0200 Subject: [PATCH 301/392] exclude message from string trait --- src/client/connection/response/certificate.rs | 8 ++++--- .../connection/response/failure/permanent.rs | 22 ++++++++++++++----- .../connection/response/failure/temporary.rs | 22 ++++++++++++++----- src/client/connection/response/input.rs | 17 +++++++------- src/client/connection/response/redirect.rs | 16 +++++++++----- 5 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 450d6d7..160e2f0 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -55,10 +55,11 @@ impl std::fmt::Display for Certificate { 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), + Self::Required { .. } => REQUIRED, + Self::NotAuthorized { .. } => NOT_AUTHORIZED, + Self::NotValid { .. } => NOT_VALID, } + .1 ) } } @@ -104,6 +105,7 @@ fn test_from_str() { assert_eq!(required.message(), Some("Message")); assert_eq!(required.to_code(), REQUIRED.0); + assert_eq!(required.to_string(), REQUIRED.1); let required = Certificate::from_str("60\r\n").unwrap(); diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index 08b6c56..e2ab9e0 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -64,13 +64,13 @@ impl std::fmt::Display for Permanent { 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), + Self::Default { .. } => DEFAULT, + Self::NotFound { .. } => NOT_FOUND, + Self::Gone { .. } => GONE, + Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED, + Self::BadRequest { .. } => BAD_REQUEST, } + .1 ) } } @@ -126,44 +126,54 @@ fn test_from_str() { let default = Permanent::from_str("50 Message\r\n").unwrap(); assert_eq!(default.message(), Some("Message")); assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); let default = Permanent::from_str("50\r\n").unwrap(); assert_eq!(default.message(), None); assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); // 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); + assert_eq!(not_found.to_string(), NOT_FOUND.1); 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); + assert_eq!(not_found.to_string(), NOT_FOUND.1); // 52 let gone = Permanent::from_str("52 Message\r\n").unwrap(); assert_eq!(gone.message(), Some("Message")); assert_eq!(gone.to_code(), GONE.0); + assert_eq!(gone.to_string(), GONE.1); let gone = Permanent::from_str("52\r\n").unwrap(); assert_eq!(gone.message(), None); assert_eq!(gone.to_code(), GONE.0); + assert_eq!(gone.to_string(), GONE.1); // 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); + assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); 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); + assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); // 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); + assert_eq!(bad_request.to_string(), BAD_REQUEST.1); 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); + assert_eq!(bad_request.to_string(), BAD_REQUEST.1); } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 363645e..768bdcd 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -64,13 +64,13 @@ impl std::fmt::Display for Temporary { 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), + Self::Default { .. } => DEFAULT, + Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, + Self::CgiError { .. } => CGI_ERROR, + Self::ProxyError { .. } => PROXY_ERROR, + Self::SlowDown { .. } => SLOW_DOWN, } + .1 ) } } @@ -126,44 +126,54 @@ fn test_from_str() { let default = Temporary::from_str("40 Message\r\n").unwrap(); assert_eq!(default.message(), Some("Message")); assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); let default = Temporary::from_str("40\r\n").unwrap(); assert_eq!(default.message(), None); assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); // 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); + assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); 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); + assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); // 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); + assert_eq!(cgi_error.to_string(), CGI_ERROR.1); 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); + assert_eq!(cgi_error.to_string(), CGI_ERROR.1); // 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); + assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); 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); + assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); // 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); + assert_eq!(slow_down.to_string(), SLOW_DOWN.1); 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); + assert_eq!(slow_down.to_string(), SLOW_DOWN.1); } diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 56dcc08..b62276b 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -46,9 +46,10 @@ impl std::fmt::Display for Input { f, "{}", match self { - Self::Default { message } => message.as_deref().unwrap_or(DEFAULT.1), - Self::Sensitive { message } => message.as_deref().unwrap_or(SENSITIVE.1), + Self::Default { .. } => DEFAULT, + Self::Sensitive { .. } => SENSITIVE, } + .1 ) } } @@ -87,23 +88,23 @@ fn test_from_str() { // 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"); + assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); 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_code(), DEFAULT.0); 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"); + assert_eq!(sensitive.to_code(), SENSITIVE.0); + assert_eq!(sensitive.to_string(), SENSITIVE.1); 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_code(), SENSITIVE.0); assert_eq!(sensitive.to_string(), SENSITIVE.1); } diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index cfb8c21..7c694fd 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -3,8 +3,8 @@ pub use error::Error; use glib::GStringPtr; -const TEMPORARY: u8 = 30; -const PERMANENT: u8 = 31; +const TEMPORARY: (u8, &str) = (30, "Temporary redirect"); +const PERMANENT: (u8, &str) = (31, "Permanent redirect"); pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection @@ -32,6 +32,7 @@ impl Redirect { Self::Permanent { .. } => PERMANENT, Self::Temporary { .. } => TEMPORARY, } + .0 } // Getters @@ -50,9 +51,10 @@ impl std::fmt::Display for Redirect { f, "{}", match self { - Self::Permanent { target } => format!("Permanent redirection to `{target}`"), - Self::Temporary { target } => format!("Temporary redirection to `{target}`"), + Self::Permanent { .. } => PERMANENT, + Self::Temporary { .. } => TEMPORARY, } + .1 ) } } @@ -104,9 +106,11 @@ fn test_from_str() { let temporary = Redirect::from_str("30 /uri\r\n").unwrap(); assert_eq!(temporary.target(), "/uri"); - assert_eq!(temporary.to_code(), TEMPORARY); + assert_eq!(temporary.to_code(), TEMPORARY.0); + assert_eq!(temporary.to_string(), TEMPORARY.1); let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); assert_eq!(permanent.target(), "/uri"); - assert_eq!(permanent.to_code(), PERMANENT); + assert_eq!(permanent.to_code(), PERMANENT.0); + assert_eq!(permanent.to_string(), PERMANENT.1); } From 788b7921674fc10cb07a8ca076c9b0257be98674 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 01:08:31 +0200 Subject: [PATCH 302/392] return connection in result --- src/client.rs | 2 +- src/client/connection.rs | 19 ++++++----- src/client/connection/response.rs | 57 ++++++++++++++++--------------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/client.rs b/src/client.rs index bcce693..f222393 100644 --- a/src/client.rs +++ b/src/client.rs @@ -60,7 +60,7 @@ impl Client { priority: Priority, cancellable: Cancellable, certificate: Option, - callback: impl FnOnce(Result) + 'static, + callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static, ) { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid diff --git a/src/client/connection.rs b/src/client/connection.rs index 9af51ed..b2cee84 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -58,7 +58,7 @@ impl Connection { request: Request, priority: Priority, cancellable: Cancellable, - callback: impl FnOnce(Result) + 'static, + callback: impl FnOnce(Result<(Response, Self), Error>) + 'static, ) { let output_stream = self.stream().output_stream(); output_stream.clone().write_async( @@ -67,14 +67,17 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match request { - Request::Gemini { .. } => { - Response::from_connection_async(self, priority, cancellable, |result| { + Request::Gemini { .. } => Response::from_connection_async( + self, + priority, + cancellable, + |result, connection| { callback(match result { - Ok(response) => Ok(response), + Ok(response) => Ok((response, connection)), Err(e) => Err(Error::Response(e)), }) - }) - } + }, + ), Request::Titan { data, .. } => output_stream.write_bytes_async( &data, priority, @@ -84,9 +87,9 @@ impl Connection { self, priority, cancellable, - |result| { + |result, connection| { callback(match result { - Ok(response) => Ok(response), + Ok(response) => Ok((response, connection)), Err(e) => Err(Error::Response(e)), }) }, diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 9f2b901..adaf209 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -36,7 +36,7 @@ impl Response { connection: Connection, priority: Priority, cancellable: Cancellable, - callback: impl FnOnce(Result) + 'static, + callback: impl FnOnce(Result, Connection) + 'static, ) { from_stream_async( Vec::with_capacity(HEADER_LEN), @@ -44,35 +44,38 @@ impl Response { cancellable, priority, |result| { - callback(match result { - Ok(buffer) => match buffer.first() { - Some(byte) => match byte { - 1 => match Input::from_utf8(&buffer) { - Ok(input) => Ok(Self::Input(input)), - Err(e) => Err(Error::Input(e)), + callback( + match result { + Ok(buffer) => match buffer.first() { + Some(byte) => match byte { + 1 => match Input::from_utf8(&buffer) { + Ok(input) => Ok(Self::Input(input)), + Err(e) => Err(Error::Input(e)), + }, + 2 => match Success::from_utf8(&buffer) { + Ok(success) => Ok(Self::Success(success)), + Err(e) => Err(Error::Success(e)), + }, + 3 => match Redirect::from_utf8(&buffer) { + Ok(redirect) => Ok(Self::Redirect(redirect)), + Err(e) => Err(Error::Redirect(e)), + }, + 4 | 5 => match Failure::from_utf8(&buffer) { + Ok(failure) => Ok(Self::Failure(failure)), + Err(e) => Err(Error::Failure(e)), + }, + 6 => match Certificate::from_utf8(&buffer) { + Ok(certificate) => Ok(Self::Certificate(certificate)), + Err(e) => Err(Error::Certificate(e)), + }, + b => Err(Error::Code(*b)), }, - 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), }, - None => Err(Error::Protocol), + Err(e) => Err(e), }, - Err(e) => Err(e), - }) + connection, + ) }, ); } From a5fbca2ace7d3556692eba1e34d0d915a5806889 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 01:20:08 +0200 Subject: [PATCH 303/392] fix route by first byte --- src/client/connection/response.rs | 12 ++++++------ src/client/connection/response/error.rs | 6 +++--- src/client/connection/response/failure.rs | 6 +++--- src/client/connection/response/failure/error.rs | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index adaf209..9c66dd3 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -48,27 +48,27 @@ impl Response { match result { Ok(buffer) => match buffer.first() { Some(byte) => match byte { - 1 => match Input::from_utf8(&buffer) { + 0x31 => match Input::from_utf8(&buffer) { Ok(input) => Ok(Self::Input(input)), Err(e) => Err(Error::Input(e)), }, - 2 => match Success::from_utf8(&buffer) { + 0x32 => match Success::from_utf8(&buffer) { Ok(success) => Ok(Self::Success(success)), Err(e) => Err(Error::Success(e)), }, - 3 => match Redirect::from_utf8(&buffer) { + 0x33 => match Redirect::from_utf8(&buffer) { Ok(redirect) => Ok(Self::Redirect(redirect)), Err(e) => Err(Error::Redirect(e)), }, - 4 | 5 => match Failure::from_utf8(&buffer) { + 0x34 | 0x35 => match Failure::from_utf8(&buffer) { Ok(failure) => Ok(Self::Failure(failure)), Err(e) => Err(Error::Failure(e)), }, - 6 => match Certificate::from_utf8(&buffer) { + 0x36 => match Certificate::from_utf8(&buffer) { Ok(certificate) => Ok(Self::Certificate(certificate)), Err(e) => Err(Error::Certificate(e)), }, - b => Err(Error::Code(*b)), + _ => Err(Error::Code), }, None => Err(Error::Protocol), }, diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index 99895d9..df8cda4 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -6,7 +6,7 @@ use std::{ #[derive(Debug)] pub enum Error { Certificate(super::certificate::Error), - Code(u8), + Code, Failure(super::failure::Error), Input(super::input::Error), Protocol, @@ -22,8 +22,8 @@ impl Display for Error { Self::Certificate(e) => { write!(f, "Certificate error: {e}") } - Self::Code(e) => { - write!(f, "Code group error: {e}*") + Self::Code => { + write!(f, "Code group error") } Self::Failure(e) => { write!(f, "Failure error: {e}") diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index c0e76ec..19f6461 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -22,15 +22,15 @@ impl Failure { pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.first() { Some(byte) => match byte { - 4 => match Temporary::from_utf8(buffer) { + 0x34 => match Temporary::from_utf8(buffer) { Ok(input) => Ok(Self::Temporary(input)), Err(e) => Err(Error::Temporary(e)), }, - 5 => match Permanent::from_utf8(buffer) { + 0x35 => match Permanent::from_utf8(buffer) { Ok(failure) => Ok(Self::Permanent(failure)), Err(e) => Err(Error::Permanent(e)), }, - b => Err(Error::Code(*b)), + _ => Err(Error::Code), }, None => Err(Error::Protocol), } diff --git a/src/client/connection/response/failure/error.rs b/src/client/connection/response/failure/error.rs index 60724eb..7725b92 100644 --- a/src/client/connection/response/failure/error.rs +++ b/src/client/connection/response/failure/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code(u8), + Code, Permanent(super::permanent::Error), Protocol, Temporary(super::temporary::Error), @@ -11,8 +11,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code(e) => { - write!(f, "Code group error: {e}*") + Self::Code => { + write!(f, "Code group error") } Self::Permanent(e) => { write!(f, "Permanent failure group error: {e}") From dc2300b1c0d8b9cd818224d21e06b6c8ead598f0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 01:47:50 +0200 Subject: [PATCH 304/392] use human-readable bytes format --- src/client/connection/response.rs | 14 ++++++-------- src/client/connection/response/failure.rs | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 9c66dd3..6c1cd7b 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -1,5 +1,3 @@ -//! Read and parse Gemini response as Object - pub mod certificate; pub mod data; // @TODO deprecated pub mod error; @@ -19,7 +17,7 @@ use super::Connection; use gio::{Cancellable, IOStream}; use glib::{object::IsA, Priority}; -const HEADER_LEN: usize = 0x400; // 1024 +const HEADER_LEN: usize = 1024; /// https://geminiprotocol.net/docs/protocol-specification.gmi#responses pub enum Response { @@ -48,23 +46,23 @@ impl Response { match result { Ok(buffer) => match buffer.first() { Some(byte) => match byte { - 0x31 => match Input::from_utf8(&buffer) { + b'1' => match Input::from_utf8(&buffer) { Ok(input) => Ok(Self::Input(input)), Err(e) => Err(Error::Input(e)), }, - 0x32 => match Success::from_utf8(&buffer) { + b'2' => match Success::from_utf8(&buffer) { Ok(success) => Ok(Self::Success(success)), Err(e) => Err(Error::Success(e)), }, - 0x33 => match Redirect::from_utf8(&buffer) { + b'3' => match Redirect::from_utf8(&buffer) { Ok(redirect) => Ok(Self::Redirect(redirect)), Err(e) => Err(Error::Redirect(e)), }, - 0x34 | 0x35 => match Failure::from_utf8(&buffer) { + b'4' | b'5' => match Failure::from_utf8(&buffer) { Ok(failure) => Ok(Self::Failure(failure)), Err(e) => Err(Error::Failure(e)), }, - 0x36 => match Certificate::from_utf8(&buffer) { + b'6' => match Certificate::from_utf8(&buffer) { Ok(certificate) => Ok(Self::Certificate(certificate)), Err(e) => Err(Error::Certificate(e)), }, diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 19f6461..40c8abf 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -22,11 +22,11 @@ impl Failure { pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.first() { Some(byte) => match byte { - 0x34 => match Temporary::from_utf8(buffer) { + b'4' => match Temporary::from_utf8(buffer) { Ok(input) => Ok(Self::Temporary(input)), Err(e) => Err(Error::Temporary(e)), }, - 0x35 => match Permanent::from_utf8(buffer) { + b'5' => match Permanent::from_utf8(buffer) { Ok(failure) => Ok(Self::Permanent(failure)), Err(e) => Err(Error::Permanent(e)), }, From 517153656b50509c5afda6440b6d611771d672b9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 02:09:41 +0200 Subject: [PATCH 305/392] update example --- README.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 23ee9de..57c3f75 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,10 @@ cargo add ggemini use gio::*; use glib::*; + use ggemini::client::{ - connection::{ - Request, Response, - response::meta::{Mime, Status} - }, - Client, Error, + connection::{response::Success, Request, Response}, + Client, }; fn main() -> ExitCode { @@ -58,20 +56,17 @@ fn main() -> ExitCode { Priority::DEFAULT, Cancellable::new(), None, // optional `GTlsCertificate` - |result: Result| match result { - Ok(response) => { - // route by status code - match response.meta.status { - // code 20, handle `GIOStream` by content type - Status::Success => match response.meta.mime.unwrap().as_str() { - // gemtext, see ggemtext crate to parse + |result| match result { + Ok((response, _connection)) => match response { + Response::Success(success) => match success { + Success::Default { mime } => match mime.as_str() { "text/gemini" => todo!(), - // other content types _ => todo!(), }, _ => todo!(), - } - } + }, + _ => todo!(), + }, Err(_) => todo!(), }, ); From 7518101b552fc58a42ce9d7baa68aa5e2ee6229f Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 02:44:33 +0200 Subject: [PATCH 306/392] implement `to_uri` method --- src/client/connection/response/redirect.rs | 62 ++++++++++++++++++- .../connection/response/redirect/error.rs | 4 ++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index 7c694fd..b0856be 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -1,7 +1,7 @@ pub mod error; pub use error::Error; -use glib::GStringPtr; +use glib::{GStringPtr, Uri, UriFlags}; const TEMPORARY: (u8, &str) = (30, "Temporary redirect"); const PERMANENT: (u8, &str) = (31, "Permanent redirect"); @@ -35,6 +35,32 @@ impl Redirect { .0 } + /// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection), + /// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base` + /// * fragment implementation uncompleted @TODO + pub fn to_uri(&self, base: &Uri) -> Result { + match Uri::build( + UriFlags::NONE, + base.scheme().as_str(), + None, // unexpected + base.host().as_deref(), + base.port(), + base.path().as_str(), + // > If a server sends a redirection in response to a request with a query string, + // > the client MUST NOT apply the query string to the new location + None, + // > A server SHOULD NOT include fragments in redirections, + // > but if one is given, and a client already has a fragment it could apply (from the original URI), + // > it is up to the client which fragment to apply. + None, // @TODO + ) + .parse_relative(self.target(), UriFlags::NONE) + { + Ok(absolute) => Ok(absolute), + Err(e) => Err(Error::Glib(e)), + } + } + // Getters pub fn target(&self) -> &str { @@ -114,3 +140,37 @@ fn test_from_str() { assert_eq!(permanent.to_code(), PERMANENT.0); assert_eq!(permanent.to_string(), PERMANENT.1); } + +#[test] +fn test_to_uri() { + use std::str::FromStr; + + let request = Uri::build( + UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("?query"), + Some("?fragment"), + ); + + let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&request).unwrap().to_string(), + "gemini://geminiprotocol.net/uri" + ); + + let resolve = Redirect::from_str("30 uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&request).unwrap().to_string(), + "gemini://geminiprotocol.net/path/uri" + ); + + let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&request).unwrap().to_string(), + "gemini://test.host/uri" + ); +} diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index b3d3579..029abf6 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -5,6 +5,7 @@ use std::{ #[derive(Debug)] pub enum Error { + Glib(glib::Error), Protocol, Target, Utf8Error(Utf8Error), @@ -13,6 +14,9 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::Glib(e) => { + write!(f, "Glib error: {e}") + } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } From d57d9fc7df9241f22ebf441473ee7658ea634461 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 02:55:42 +0200 Subject: [PATCH 307/392] update enum name --- src/client/connection/response/redirect.rs | 2 +- src/client/connection/response/redirect/error.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index b0856be..8e1cb24 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -57,7 +57,7 @@ impl Redirect { .parse_relative(self.target(), UriFlags::NONE) { Ok(absolute) => Ok(absolute), - Err(e) => Err(Error::Glib(e)), + Err(e) => Err(Error::Uri(e)), } } diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index 029abf6..acee073 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -5,7 +5,7 @@ use std::{ #[derive(Debug)] pub enum Error { - Glib(glib::Error), + Uri(glib::Error), Protocol, Target, Utf8Error(Utf8Error), @@ -14,8 +14,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Glib(e) => { - write!(f, "Glib error: {e}") + Self::Uri(e) => { + write!(f, "URI error: {e}") } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") From 9bb926f243d51717b230245b4b6c61c43b3afe93 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 03:03:36 +0200 Subject: [PATCH 308/392] update example --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 57c3f75..98fe828 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ use glib::*; use ggemini::client::{ - connection::{response::Success, Request, Response}, + connection::{Request, Response}, Client, }; @@ -59,11 +59,10 @@ fn main() -> ExitCode { |result| match result { Ok((response, _connection)) => match response { Response::Success(success) => match success { - Success::Default { mime } => match mime.as_str() { + _ => match success.mime() { "text/gemini" => todo!(), _ => todo!(), - }, - _ => todo!(), + } }, _ => todo!(), }, From 041454d8df074405cf30291c6880b1d0612255a7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 03:10:25 +0200 Subject: [PATCH 309/392] update example --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 98fe828..90105f5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ cargo add ggemini use gio::*; use glib::*; - use ggemini::client::{ connection::{Request, Response}, Client, @@ -58,11 +57,9 @@ fn main() -> ExitCode { None, // optional `GTlsCertificate` |result| match result { Ok((response, _connection)) => match response { - Response::Success(success) => match success { - _ => match success.mime() { - "text/gemini" => todo!(), - _ => todo!(), - } + Response::Success(success) => match success.mime() { + "text/gemini" => todo!(), + _ => todo!(), }, _ => todo!(), }, From 998a4e97b46283faa5e39509551d4611e33948bf Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 11:27:10 +0200 Subject: [PATCH 310/392] fix uri arguments --- src/client/connection/response/redirect.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index 8e1cb24..add308c 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -152,8 +152,8 @@ fn test_to_uri() { Some("geminiprotocol.net"), -1, "/path/", - Some("?query"), - Some("?fragment"), + Some("query"), + Some("fragment"), ); let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); From c9d5e5987cb4e86dcb2900e027a62aacee6c6c3e Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:05:37 +0200 Subject: [PATCH 311/392] minor optimizations --- src/gio/memory_input_stream.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 30f9fd8..f268ec6 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -40,19 +40,12 @@ pub fn move_all_from_stream_async( memory_input_stream: MemoryInputStream, cancellable: Cancellable, priority: Priority, - bytes: ( - usize, // bytes_in_chunk - usize, // bytes_total_limit - usize, // bytes_total - ), - callback: ( - impl Fn(Bytes, usize) + 'static, // on_chunk - impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, // on_complete + (bytes_in_chunk, bytes_total_limit, mut bytes_total): (usize, usize, usize), + (on_chunk, on_complete): ( + impl Fn(Bytes, usize) + 'static, + impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { - let (on_chunk, on_complete) = callback; - let (bytes_in_chunk, bytes_total_limit, bytes_total) = bytes; - base_io_stream.input_stream().read_bytes_async( bytes_in_chunk, priority, @@ -60,7 +53,7 @@ pub fn move_all_from_stream_async( move |result| match result { Ok(bytes) => { // Update bytes total - let bytes_total = bytes_total + bytes.len(); + bytes_total += bytes.len(); // Callback chunk function on_chunk(bytes.clone(), bytes_total); @@ -92,5 +85,5 @@ pub fn move_all_from_stream_async( on_complete(Err(Error::InputStream(e))); } }, - ); + ) } From 1505b6311c7f03756e6880667afb1110f7fc321d Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:38:04 +0200 Subject: [PATCH 312/392] rename arguments, use tuple for arguments group --- src/gio/memory_input_stream.rs | 49 +++++++++++++++------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index f268ec6..8404f7f 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -5,7 +5,7 @@ use gio::{ prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, Cancellable, IOStream, MemoryInputStream, }; -use glib::{object::IsA, Bytes, Priority}; +use glib::{object::IsA, Priority}; /// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) @@ -14,20 +14,21 @@ use glib::{object::IsA, Bytes, Priority}; /// * safe read (of memory overflow) to dynamically allocated buffer, where final size of target data unknown /// * calculate bytes processed on chunk load pub fn from_stream_async( - base_io_stream: impl IsA, + io_stream: impl IsA, cancelable: Cancellable, priority: Priority, - bytes_in_chunk: usize, - bytes_total_limit: usize, - on_chunk: impl Fn(Bytes, usize) + 'static, - on_complete: impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, + (chunk, limit): (usize, usize), + (on_chunk, on_complete): ( + impl Fn(usize, usize) + 'static, + impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, + ), ) { move_all_from_stream_async( - base_io_stream, + io_stream, MemoryInputStream::new(), cancelable, priority, - (bytes_in_chunk, bytes_total_limit, 0), + (chunk, limit, 0), (on_chunk, on_complete), ); } @@ -36,48 +37,42 @@ pub fn from_stream_async( /// to [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread pub fn move_all_from_stream_async( - base_io_stream: impl IsA, + io_stream: impl IsA, memory_input_stream: MemoryInputStream, cancellable: Cancellable, priority: Priority, - (bytes_in_chunk, bytes_total_limit, mut bytes_total): (usize, usize, usize), + (chunk, limit, mut total): (usize, usize, usize), (on_chunk, on_complete): ( - impl Fn(Bytes, usize) + 'static, + impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { - base_io_stream.input_stream().read_bytes_async( - bytes_in_chunk, + io_stream.input_stream().read_bytes_async( + chunk, priority, Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - // Update bytes total - bytes_total += bytes.len(); + total += bytes.len(); + on_chunk(bytes.len(), total); - // Callback chunk function - on_chunk(bytes.clone(), bytes_total); - - // Validate max size - if bytes_total > bytes_total_limit { - return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); + if total > limit { + return on_complete(Err(Error::BytesTotal(total, limit))); } - // No bytes were read, end of stream if bytes.len() == 0 { - return on_complete(Ok((memory_input_stream, bytes_total))); + return on_complete(Ok((memory_input_stream, total))); } - // Write chunk bytes memory_input_stream.add_bytes(&bytes); - // Continue + // continue reading.. move_all_from_stream_async( - base_io_stream, + io_stream, memory_input_stream, cancellable, priority, - (bytes_in_chunk, bytes_total_limit, bytes_total), + (chunk, limit, total), (on_chunk, on_complete), ); } From 4ee92645ca090d81712239d436831f1baf867187 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:38:34 +0200 Subject: [PATCH 313/392] remove deprecated features --- src/client/connection/response.rs | 1 - src/client/connection/response/data.rs | 7 -- src/client/connection/response/data/text.rs | 113 ------------------ .../connection/response/data/text/error.rs | 24 ---- 4 files changed, 145 deletions(-) delete mode 100644 src/client/connection/response/data.rs delete mode 100644 src/client/connection/response/data/text.rs delete mode 100644 src/client/connection/response/data/text/error.rs diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 6c1cd7b..51c3c7e 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -1,5 +1,4 @@ pub mod certificate; -pub mod data; // @TODO deprecated pub mod error; pub mod failure; pub mod input; diff --git a/src/client/connection/response/data.rs b/src/client/connection/response/data.rs deleted file mode 100644 index 0009fe8..0000000 --- a/src/client/connection/response/data.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! 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; diff --git a/src/client/connection/response/data/text.rs b/src/client/connection/response/data/text.rs deleted file mode 100644 index aaa8aa1..0000000 --- a/src/client/connection/response/data/text.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! 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(String); - -impl Default for Text { - fn default() -> Self { - Self::new() - } -} - -impl Text { - // Constructors - - /// Create new `Self` - pub fn new() -> Self { - Self(String::new()) - } - - /// Create new `Self` from string - pub fn from_string(data: &str) -> Self { - Self(data.into()) - } - - /// Create new `Self` from UTF-8 buffer - pub fn from_utf8(buffer: &[u8]) -> Result { - 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, - priority: Priority, - cancellable: Cancellable, - on_complete: impl FnOnce(Result) + '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)), - }, - ); - } -} - -impl std::fmt::Display for Text { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -// 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, - stream: impl IsA, - cancelable: Cancellable, - priority: Priority, - callback: impl FnOnce(Result, 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))), - }, - ); -} diff --git a/src/client/connection/response/data/text/error.rs b/src/client/connection/response/data/text/error.rs deleted file mode 100644 index 4a853aa..0000000 --- a/src/client/connection/response/data/text/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -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}") - } - } - } -} From 46483d1829c61260e0591de73d23bf30b1565f67 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:48:57 +0200 Subject: [PATCH 314/392] rename methods, change arguments order --- src/gio/memory_input_stream.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 8404f7f..c953bc5 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -15,19 +15,19 @@ use glib::{object::IsA, Priority}; /// * calculate bytes processed on chunk load pub fn from_stream_async( io_stream: impl IsA, - cancelable: Cancellable, priority: Priority, + cancelable: Cancellable, (chunk, limit): (usize, usize), (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { - move_all_from_stream_async( - io_stream, + for_memory_input_stream_async( MemoryInputStream::new(), - cancelable, + io_stream, priority, + cancelable, (chunk, limit, 0), (on_chunk, on_complete), ); @@ -36,11 +36,11 @@ pub fn from_stream_async( /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread -pub fn move_all_from_stream_async( - io_stream: impl IsA, +pub fn for_memory_input_stream_async( memory_input_stream: MemoryInputStream, - cancellable: Cancellable, + io_stream: impl IsA, priority: Priority, + cancellable: Cancellable, (chunk, limit, mut total): (usize, usize, usize), (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, @@ -67,14 +67,14 @@ pub fn move_all_from_stream_async( memory_input_stream.add_bytes(&bytes); // continue reading.. - move_all_from_stream_async( - io_stream, + for_memory_input_stream_async( memory_input_stream, - cancellable, + io_stream, priority, + cancellable, (chunk, limit, total), (on_chunk, on_complete), - ); + ) } Err(e) => { on_complete(Err(Error::InputStream(e))); From 867945ec74b864eb4c9a706a436e7d0d678b4464 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:55:31 +0200 Subject: [PATCH 315/392] rename method, apply minor optimizations --- src/gio/file_output_stream.rs | 47 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index b34b507..ce5fac1 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -10,49 +10,40 @@ use glib::{object::IsA, Bytes, Priority}; /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread -pub fn move_all_from_stream_async( - base_io_stream: impl IsA, +pub fn from_stream_async( + io_stream: impl IsA, file_output_stream: FileOutputStream, cancellable: Cancellable, priority: Priority, - bytes: ( + (chunk, limit, mut total): ( usize, // bytes_in_chunk Option, // bytes_total_limit, `None` to unlimited usize, // bytes_total ), - callback: ( + (on_chunk, on_complete): ( impl Fn(Bytes, usize) + 'static, // on_chunk impl FnOnce(Result<(FileOutputStream, usize), Error>) + 'static, // on_complete ), ) { - let (on_chunk, on_complete) = callback; - let (bytes_in_chunk, bytes_total_limit, bytes_total) = bytes; - - base_io_stream.input_stream().read_bytes_async( - bytes_in_chunk, + io_stream.input_stream().read_bytes_async( + chunk, priority, Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - // Update bytes total - let bytes_total = bytes_total + bytes.len(); + total += bytes.len(); + on_chunk(bytes.clone(), total); - // Callback chunk function - on_chunk(bytes.clone(), bytes_total); - - // Validate max size - if let Some(bytes_total_limit) = bytes_total_limit { - if bytes_total > bytes_total_limit { - return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); + if let Some(limit) = limit { + if total > limit { + return on_complete(Err(Error::BytesTotal(total, limit))); } } - // No bytes were read, end of stream if bytes.len() == 0 { - return on_complete(Ok((file_output_stream, bytes_total))); + return on_complete(Ok((file_output_stream, total))); } - // Write chunk bytes file_output_stream.clone().write_async( bytes.clone(), priority, @@ -60,13 +51,13 @@ pub fn move_all_from_stream_async( move |result| { match result { Ok(_) => { - // Continue - move_all_from_stream_async( - base_io_stream, + // continue read.. + from_stream_async( + io_stream, file_output_stream, cancellable, priority, - (bytes_in_chunk, bytes_total_limit, bytes_total), + (chunk, limit, total), (on_chunk, on_complete), ); } @@ -77,9 +68,7 @@ pub fn move_all_from_stream_async( }, ); } - Err(e) => { - on_complete(Err(Error::InputStream(e))); - } + Err(e) => on_complete(Err(Error::InputStream(e))), }, - ); + ) } From a9536011417d443429565a44995a22aa6c3995d7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:59:40 +0200 Subject: [PATCH 316/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f54e4c8..d4c6b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.15.0" +version = "0.15.1" edition = "2021" license = "MIT" readme = "README.md" From b3e3f2e07b7a2a79f60323c8b300340ab668f254 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 14:02:19 +0200 Subject: [PATCH 317/392] rollback release version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d4c6b32..f54e4c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.15.1" +version = "0.15.0" edition = "2021" license = "MIT" readme = "README.md" From d4f076f074647c63f0a4fc5647e929d738f0fbc7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 14:04:49 +0200 Subject: [PATCH 318/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f54e4c8..d4c6b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.15.0" +version = "0.15.1" edition = "2021" license = "MIT" readme = "README.md" From 62f53304aaceb450f48bb29d19e24f647f66a8ad Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 9 Feb 2025 00:42:57 +0200 Subject: [PATCH 319/392] stop chunk iteration on match `len < chunk` condition (some servers may close the connection immediately); hold `memory_input_stream` in the error returned --- Cargo.toml | 2 +- src/gio/memory_input_stream.rs | 30 +++++++++++++++++----------- src/gio/memory_input_stream/error.rs | 8 ++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d4c6b32..25b5fde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.15.1" +version = "0.16.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index c953bc5..c588f9d 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -53,19 +53,27 @@ pub fn for_memory_input_stream_async( Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - total += bytes.len(); - on_chunk(bytes.len(), total); + let len = bytes.len(); // calculate once - if total > limit { - return on_complete(Err(Error::BytesTotal(total, limit))); - } - - if bytes.len() == 0 { - return on_complete(Ok((memory_input_stream, total))); - } + total += len; + on_chunk(len, total); memory_input_stream.add_bytes(&bytes); + // prevent memory overflow on size `limit` reached + // * add last received bytes into the `memory_input_stream` anyway (to prevent data lost), + // it's safe because limited to the `chunk` size + if total > limit { + return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); + } + + // is the next iteration required? + if len < chunk // some servers may close the connection after first chunk request (@TODO this condition wants review) + || len == 0 + { + return on_complete(Ok((memory_input_stream, total))); + } + // continue reading.. for_memory_input_stream_async( memory_input_stream, @@ -76,9 +84,7 @@ pub fn for_memory_input_stream_async( (on_chunk, on_complete), ) } - Err(e) => { - on_complete(Err(Error::InputStream(e))); - } + Err(e) => on_complete(Err(Error::InputStream(memory_input_stream, e))), }, ) } diff --git a/src/gio/memory_input_stream/error.rs b/src/gio/memory_input_stream/error.rs index 673906c..6b8ae86 100644 --- a/src/gio/memory_input_stream/error.rs +++ b/src/gio/memory_input_stream/error.rs @@ -2,17 +2,17 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - BytesTotal(usize, usize), - InputStream(glib::Error), + BytesTotal(gio::MemoryInputStream, usize, usize), + InputStream(gio::MemoryInputStream, glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::BytesTotal(total, limit) => { + Self::BytesTotal(_, total, limit) => { write!(f, "Bytes total limit reached: {total} / {limit}") } - Self::InputStream(e) => { + Self::InputStream(_, e) => { write!(f, "Input stream error: {e}") } } From bb8c2273d4080eaa3cea4f9fd386230adceffbc9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 9 Feb 2025 02:02:55 +0200 Subject: [PATCH 320/392] remove unspecified condition, skip handle the chunk on zero bytes received --- src/gio/memory_input_stream.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index c588f9d..2500087 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -55,9 +55,16 @@ pub fn for_memory_input_stream_async( Ok(bytes) => { let len = bytes.len(); // calculate once + // is the end of stream + if len == 0 { + return on_complete(Ok((memory_input_stream, total))); + } + + // handle the chunk total += len; on_chunk(len, total); + // push bytes into the memory pool memory_input_stream.add_bytes(&bytes); // prevent memory overflow on size `limit` reached @@ -67,13 +74,6 @@ pub fn for_memory_input_stream_async( return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); } - // is the next iteration required? - if len < chunk // some servers may close the connection after first chunk request (@TODO this condition wants review) - || len == 0 - { - return on_complete(Ok((memory_input_stream, total))); - } - // continue reading.. for_memory_input_stream_async( memory_input_stream, From 582744f8304819567a6d110a891aaf42e166d19e Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 9 Feb 2025 02:50:34 +0200 Subject: [PATCH 321/392] update comments --- src/gio/memory_input_stream.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 2500087..def3845 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -55,26 +55,24 @@ pub fn for_memory_input_stream_async( Ok(bytes) => { let len = bytes.len(); // calculate once - // is the end of stream + // is end of stream if len == 0 { return on_complete(Ok((memory_input_stream, total))); } - // handle the chunk + // callback chunk function total += len; on_chunk(len, total); // push bytes into the memory pool memory_input_stream.add_bytes(&bytes); - // prevent memory overflow on size `limit` reached - // * add last received bytes into the `memory_input_stream` anyway (to prevent data lost), - // it's safe because limited to the `chunk` size + // prevent memory overflow if total > limit { return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); } - // continue reading.. + // handle next chunk.. for_memory_input_stream_async( memory_input_stream, io_stream, From 83b29c027660ca402ccaf46a99f1bbf21df0b907 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 10 Feb 2025 00:17:08 +0200 Subject: [PATCH 322/392] use common callback --- src/client.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index f222393..79f51c4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -82,9 +82,11 @@ impl Client { request, priority, cancellable, - move |result| match result { - Ok(response) => callback(Ok(response)), - Err(e) => callback(Err(Error::Connection(e))), + move |result| { + callback(match result { + Ok(response) => Ok(response), + Err(e) => Err(Error::Connection(e)), + }) }, ), Err(e) => callback(Err(Error::Connection(e))), From 0f1caadc03bb24a30de7c819073e232bc895d94e Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 10 Feb 2025 01:57:35 +0200 Subject: [PATCH 323/392] update comment --- src/client/connection.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index b2cee84..2ad832e 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -129,7 +129,9 @@ pub fn new_tls_client_connection( // Prevent session resumption (certificate change ability in runtime) tls_client_connection.set_property("session-resumption-enabled", is_session_resumption); - // @TODO handle + // Return `Err` on server connection mismatch following specification lines: + // > Gemini servers MUST use the TLS close_notify implementation to close the connection + // > A client SHOULD notify the user of such a case // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections tls_client_connection.set_require_close_notify(true); From 8334d8a83ca640610412910095f3390f4fa175f7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 10 Feb 2025 02:00:54 +0200 Subject: [PATCH 324/392] update comment --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 2ad832e..ec29195 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -118,7 +118,7 @@ impl Connection { /// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) /// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) -/// using `server_identity` as [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( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, From 9ce509cedc8f2a92bd5bb9125c4d219f4f38d48b Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 10 Feb 2025 02:02:45 +0200 Subject: [PATCH 325/392] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 90105f5..cd21d88 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) and/or [gio](https://crates.io/crates/gio) (`2.66+`) bindings. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) / [gio](https://crates.io/crates/gio) (`2.66+`) backend. ## Requirements From 862ab1ccfa6a6ca97507bbf5eda86f6d8446363c Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 13 Feb 2025 07:14:41 +0200 Subject: [PATCH 326/392] increase default timeout to 30 seconds --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 79f51c4..342eac3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,7 +12,7 @@ use glib::Priority; // Defaults -pub const DEFAULT_TIMEOUT: u32 = 10; +pub const DEFAULT_TIMEOUT: u32 = 30; pub const DEFAULT_SESSION_RESUMPTION: bool = false; /// Main point where connect external crate From e635c410654a96d1373f684de0bdcd902082a3ae Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Feb 2025 21:11:41 +0200 Subject: [PATCH 327/392] add funding info --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ada8a24 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://yggverse.github.io/#donate \ No newline at end of file From f51c636401e7e8f38c8da155d9d2d27372d659fa Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Feb 2025 23:11:17 +0200 Subject: [PATCH 328/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 25b5fde..19c3745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.16.0" +version = "0.16.1" edition = "2021" license = "MIT" readme = "README.md" From 4f6799a495dcfdecfaddfd2afbe4e8ed05370609 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Feb 2025 07:38:09 +0200 Subject: [PATCH 329/392] remove extra clone --- src/gio/file_output_stream.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index ce5fac1..9a62667 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -61,9 +61,7 @@ pub fn from_stream_async( (on_chunk, on_complete), ); } - Err((bytes, e)) => { - on_complete(Err(Error::OutputStream(bytes.clone(), e))) - } + Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))), } }, ); From 1ff38ee83813d28f622b3acfae51a9563bfd2f62 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Feb 2025 07:49:41 +0200 Subject: [PATCH 330/392] fix maximum payload of 16 kB by using `write_all` method, hold bytes on request error --- src/client/connection.rs | 22 ++++++++++++++-------- src/client/connection/error.rs | 4 ++-- src/gio/file_output_stream.rs | 5 ++++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index ec29195..b7b2eb9 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -9,12 +9,12 @@ pub use response::Response; // Local dependencies use gio::{ - prelude::{IOStreamExt, OutputStreamExt, OutputStreamExtManual, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, }; use glib::{ object::{Cast, ObjectExt}, - Priority, + Bytes, Priority, }; pub struct Connection { @@ -61,8 +61,11 @@ impl Connection { callback: impl FnOnce(Result<(Response, Self), Error>) + 'static, ) { let output_stream = self.stream().output_stream(); - output_stream.clone().write_async( - request.header().into_bytes(), + // Make sure **all header bytes** sent to the destination + // > 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 + output_stream.clone().write_all_async( + Bytes::from_owned(request.header()), priority, Some(&cancellable.clone()), move |result| match result { @@ -78,8 +81,11 @@ impl Connection { }) }, ), - Request::Titan { data, .. } => output_stream.write_bytes_async( - &data, + // Make sure **all data bytes** sent to the destination + // > 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 + Request::Titan { data, .. } => output_stream.write_all_async( + data, priority, Some(&cancellable.clone()), move |result| match result { @@ -94,11 +100,11 @@ impl Connection { }) }, ), - Err(e) => callback(Err(Error::Request(e))), + Err((b, e)) => callback(Err(Error::Request(b, e))), }, ), }, - Err((_, e)) => callback(Err(Error::Request(e))), + Err((b, e)) => callback(Err(Error::Request(b, e))), }, ) } diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 178cfba..711c2b6 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Request(glib::Error), + Request(glib::Bytes, glib::Error), Response(crate::client::connection::response::Error), TlsClientConnection(glib::Error), } @@ -10,7 +10,7 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Request(e) => { + Self::Request(_, e) => { write!(f, "Request error: {e}") } Self::Response(e) => { diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 9a62667..9d6ef23 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -44,7 +44,10 @@ pub fn from_stream_async( return on_complete(Ok((file_output_stream, total))); } - file_output_stream.clone().write_async( + // Make sure **all bytes** sent to the destination + // > 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 + file_output_stream.clone().write_all_async( bytes.clone(), priority, Some(&cancellable.clone()), From 564f5b69d5605f82387ddc801d61d4ee3842a007 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Feb 2025 07:50:49 +0200 Subject: [PATCH 331/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 19c3745..39e2fd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.16.1" +version = "0.17.0" edition = "2021" license = "MIT" readme = "README.md" From e3abd89c9d4b2c3382cbbafc8a226dd74a6398ec Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Feb 2025 07:52:43 +0200 Subject: [PATCH 332/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 39e2fd8..9eba89d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.0" +version = "0.17.1" edition = "2021" license = "MIT" readme = "README.md" From 0523f678503768378458a65dd326eef42d76d6b7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 13:16:14 +0200 Subject: [PATCH 333/392] add support for uri starts with double slash --- src/client/connection/response/redirect.rs | 123 +++++++++++------- .../connection/response/redirect/error.rs | 4 + 2 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index add308c..aa5e8e8 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -54,8 +54,32 @@ impl Redirect { // > it is up to the client which fragment to apply. None, // @TODO ) - .parse_relative(self.target(), UriFlags::NONE) - { + .parse_relative( + &{ + // URI started with double slash yet not supported by Glib function + // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + let t = self.target(); + match t.strip_prefix("//") { + Some(p) => { + let postfix = p.trim_start_matches(":"); + format!( + "{}://{}", + base.scheme(), + if postfix.is_empty() { + match base.host() { + Some(h) => format!("{h}/"), + None => return Err(Error::BaseHost), + } + } else { + postfix.to_string() + } + ) + } + None => t.to_string(), + } + }, + UriFlags::NONE, + ) { Ok(absolute) => Ok(absolute), Err(e) => Err(Error::Uri(e)), } @@ -127,50 +151,59 @@ fn target(value: Option<&GStringPtr>) -> Result { } #[test] -fn test_from_str() { +fn test() { 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.0); + assert_eq!(temporary.to_string(), TEMPORARY.1); - let temporary = Redirect::from_str("30 /uri\r\n").unwrap(); - assert_eq!(temporary.target(), "/uri"); - assert_eq!(temporary.to_code(), TEMPORARY.0); - assert_eq!(temporary.to_string(), TEMPORARY.1); + let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); + assert_eq!(permanent.target(), "/uri"); + assert_eq!(permanent.to_code(), PERMANENT.0); + assert_eq!(permanent.to_string(), PERMANENT.1); + } + { + let base = Uri::build( + UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); - let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); - assert_eq!(permanent.target(), "/uri"); - assert_eq!(permanent.to_code(), PERMANENT.0); - assert_eq!(permanent.to_string(), PERMANENT.1); -} - -#[test] -fn test_to_uri() { - use std::str::FromStr; - - let request = Uri::build( - UriFlags::NONE, - "gemini", - None, - Some("geminiprotocol.net"), - -1, - "/path/", - Some("query"), - Some("fragment"), - ); - - let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://geminiprotocol.net/uri" - ); - - let resolve = Redirect::from_str("30 uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://geminiprotocol.net/path/uri" - ); - - let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://test.host/uri" - ); + let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://geminiprotocol.net/uri" + ); + + let resolve = Redirect::from_str("30 uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://geminiprotocol.net/path/uri" + ); + + let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://test.host/uri" + ); + + let resolve = Redirect::from_str("30 //\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://geminiprotocol.net/" + ); + + let resolve = Redirect::from_str("30 //:\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://geminiprotocol.net/" + ); + } } diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index acee073..eeaf2ed 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -5,6 +5,7 @@ use std::{ #[derive(Debug)] pub enum Error { + BaseHost, Uri(glib::Error), Protocol, Target, @@ -14,6 +15,9 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::BaseHost => { + write!(f, "Base host required") + } Self::Uri(e) => { write!(f, "URI error: {e}") } From 06fc69cff8556d42ff79286d7c3194e4b098eb45 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 14:44:30 +0200 Subject: [PATCH 334/392] update dependencies version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9eba89d..8c1d0e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.20.4" +version = "0.20.9" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.20.4" +version = "0.20.9" features = ["v2_66"] From d7166dac66d5a3c0bd88483890435f358c444cfa Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 14:49:30 +0200 Subject: [PATCH 335/392] update tests --- src/client/connection/response/redirect.rs | 48 +++++++++++++++------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index aa5e8e8..2554fdf 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -175,34 +175,52 @@ fn test() { Some("query"), Some("fragment"), ); - - let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 /uri\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://geminiprotocol.net/uri" ); - - let resolve = Redirect::from_str("30 uri\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 uri\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://geminiprotocol.net/path/uri" ); - - let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 gemini://test.host/uri\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://test.host/uri" ); - - let resolve = Redirect::from_str("30 //\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 //\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://geminiprotocol.net/" ); - - let resolve = Redirect::from_str("30 //:\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 //geminiprotocol.net/path\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), + "gemini://geminiprotocol.net/path" + ); + assert_eq!( + Redirect::from_str("30 //:\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://geminiprotocol.net/" ); } From 90cc58ab9286003015270702e76b1cf3e69b93d2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 17:11:44 +0200 Subject: [PATCH 336/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8c1d0e1..b865b2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.1" +version = "0.17.2" edition = "2021" license = "MIT" readme = "README.md" From fc8356f7ac8daef99b87594c1bb4c4593af646f5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 21:31:22 +0200 Subject: [PATCH 337/392] update rust version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b865b2d..8fc341c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ggemini" version = "0.17.2" -edition = "2021" +edition = "2024" license = "MIT" readme = "README.md" description = "Glib/Gio-oriented network API for Gemini protocol" From e6661c1d00b4fd96c415867246227b99df29bbf7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 21:58:00 +0200 Subject: [PATCH 338/392] apply new fmt version --- src/client.rs | 2 +- src/client/connection.rs | 4 ++-- src/client/connection/response.rs | 2 +- src/gio/file_output_stream.rs | 4 ++-- src/gio/memory_input_stream.rs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index 342eac3..be66b5e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ pub mod error; pub use connection::{Connection, Request, Response}; pub use error::Error; -use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate}; +use gio::{Cancellable, SocketClient, SocketProtocol, TlsCertificate, prelude::SocketClientExt}; use glib::Priority; // Defaults diff --git a/src/client/connection.rs b/src/client/connection.rs index b7b2eb9..267077c 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -9,12 +9,12 @@ pub use response::Response; // Local dependencies use gio::{ - prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, + prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, }; use glib::{ - object::{Cast, ObjectExt}, Bytes, Priority, + object::{Cast, ObjectExt}, }; pub struct Connection { diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 51c3c7e..9a16679 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -14,7 +14,7 @@ pub use success::Success; use super::Connection; use gio::{Cancellable, IOStream}; -use glib::{object::IsA, Priority}; +use glib::{Priority, object::IsA}; const HEADER_LEN: usize = 1024; diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 9d6ef23..7d9415c 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -2,10 +2,10 @@ pub mod error; pub use error::Error; use gio::{ - prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual}, Cancellable, FileOutputStream, IOStream, + prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual}, }; -use glib::{object::IsA, Bytes, Priority}; +use glib::{Bytes, Priority, object::IsA}; /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index def3845..21c6337 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -2,10 +2,10 @@ pub mod error; pub use error::Error; use gio::{ - prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, Cancellable, IOStream, MemoryInputStream, + prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, }; -use glib::{object::IsA, Priority}; +use glib::{Priority, object::IsA}; /// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) From 0aeb501760d6a52a695447b420a46ce191fb180f Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 22:01:40 +0200 Subject: [PATCH 339/392] apply new version requirements --- src/client/connection/request.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 46576a3..75a4927 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -39,10 +39,10 @@ impl Request { uri.to_string_partial(UriHideFlags::QUERY), data.len() ); - if let Some(ref mime) = mime { + if let Some(mime) = mime { header.push_str(&format!(";mime={mime}")); } - if let Some(ref token) = token { + if let Some(token) = token { header.push_str(&format!(";token={token}")); } if let Some(query) = uri.query() { From af8a972cca4db570f689edb29fe0373b5f73a984 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 18 Mar 2025 00:48:58 +0200 Subject: [PATCH 340/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8fc341c..54dd4b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.2" +version = "0.17.3" edition = "2024" license = "MIT" readme = "README.md" From 5bb52fbd8c5aec9596c02ac641f10606407f0cd2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 01:16:51 +0200 Subject: [PATCH 341/392] hold raw header string --- src/client/connection/response/certificate.rs | 33 +++++++++--- src/client/connection/response/failure.rs | 7 +++ .../connection/response/failure/permanent.rs | 51 +++++++++++++++---- .../connection/response/failure/temporary.rs | 51 +++++++++++++++---- src/client/connection/response/input.rs | 22 ++++++-- src/client/connection/response/redirect.rs | 15 ++++-- src/client/connection/response/success.rs | 13 +++-- 7 files changed, 155 insertions(+), 37 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 160e2f0..e9f76c7 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -9,11 +9,20 @@ const NOT_VALID: (u8, &str) = (11, "Certificate not valid"); /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates pub enum Certificate { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - Required { message: Option }, + Required { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - NotAuthorized { message: Option }, + NotAuthorized { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - NotValid { message: Option }, + NotValid { + header: String, + message: Option, + }, } impl Certificate { @@ -39,11 +48,20 @@ impl Certificate { .0 } + pub fn header(&self) -> &str { + match self { + Self::Required { header, .. } + | Self::NotAuthorized { header, .. } + | Self::NotValid { header, .. } => header, + } + .as_str() + } + pub fn message(&self) -> Option<&str> { match self { - Self::Required { message } => message, - Self::NotAuthorized { message } => message, - Self::NotValid { message } => message, + Self::Required { message, .. } + | Self::NotAuthorized { message, .. } + | Self::NotValid { message, .. } => message, } .as_deref() } @@ -69,16 +87,19 @@ impl std::str::FromStr for Certificate { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("60") { return Ok(Self::Required { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("61") { return Ok(Self::NotAuthorized { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("62") { return Ok(Self::NotValid { + header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 40c8abf..8e85e84 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -45,6 +45,13 @@ impl Failure { } } + pub fn header(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.header(), + Self::Temporary(temporary) => temporary.header(), + } + } + pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e2ab9e0..e69510c 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -10,15 +10,30 @@ 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 }, + Default { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found - NotFound { message: Option }, + NotFound { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone - Gone { message: Option }, + Gone { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused - ProxyRequestRefused { message: Option }, + ProxyRequestRefused { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request - BadRequest { message: Option }, + BadRequest { + header: String, + message: Option, + }, } impl Permanent { @@ -46,13 +61,24 @@ impl Permanent { .0 } + pub fn header(&self) -> &str { + match self { + Self::Default { header, .. } + | Self::NotFound { header, .. } + | Self::Gone { header, .. } + | Self::ProxyRequestRefused { header, .. } + | Self::BadRequest { header, .. } => header, + } + .as_str() + } + 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, + Self::Default { message, .. } + | Self::NotFound { message, .. } + | Self::Gone { message, .. } + | Self::ProxyRequestRefused { message, .. } + | Self::BadRequest { message, .. } => message, } .as_deref() } @@ -80,26 +106,31 @@ impl std::str::FromStr for Permanent { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("50") { return Ok(Self::Default { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("51") { return Ok(Self::NotFound { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("52") { return Ok(Self::Gone { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("53") { return Ok(Self::ProxyRequestRefused { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("59") { return Ok(Self::BadRequest { + header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 768bdcd..c9d8083 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -10,15 +10,30 @@ 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 }, + Default { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable { message: Option }, + ServerUnavailable { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError { message: Option }, + CgiError { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError { message: Option }, + ProxyError { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown { message: Option }, + SlowDown { + header: String, + message: Option, + }, } impl Temporary { @@ -46,13 +61,24 @@ impl Temporary { .0 } + pub fn header(&self) -> &str { + match self { + Self::Default { header, .. } + | Self::ServerUnavailable { header, .. } + | Self::CgiError { header, .. } + | Self::ProxyError { header, .. } + | Self::SlowDown { header, .. } => header, + } + .as_str() + } + 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, + Self::Default { message, .. } + | Self::ServerUnavailable { message, .. } + | Self::CgiError { message, .. } + | Self::ProxyError { message, .. } + | Self::SlowDown { message, .. } => message, } .as_deref() } @@ -80,26 +106,31 @@ impl std::str::FromStr for Temporary { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("40") { return Ok(Self::Default { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("41") { return Ok(Self::ServerUnavailable { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("42") { return Ok(Self::CgiError { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("43") { return Ok(Self::ProxyError { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("44") { return Ok(Self::SlowDown { + header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index b62276b..c524b9a 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -5,8 +5,14 @@ const DEFAULT: (u8, &str) = (10, "Input"); const SENSITIVE: (u8, &str) = (11, "Sensitive input"); pub enum Input { - Default { message: Option }, - Sensitive { message: Option }, + Default { + header: String, + message: Option, + }, + Sensitive { + header: String, + message: Option, + }, } impl Input { @@ -31,10 +37,16 @@ impl Input { .0 } + pub fn header(&self) -> &str { + match self { + Self::Default { header, .. } | Self::Sensitive { header, .. } => header, + } + .as_str() + } + pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::Sensitive { message } => message, + Self::Default { message, .. } | Self::Sensitive { message, .. } => message, } .as_deref() } @@ -59,11 +71,13 @@ impl std::str::FromStr for Input { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("10") { return Ok(Self::Default { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("11") { return Ok(Self::Sensitive { + header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index 2554fdf..cc838df 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -8,9 +8,9 @@ const PERMANENT: (u8, &str) = (31, "Permanent redirect"); pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - Temporary { target: String }, + Temporary { header: String, target: String }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Permanent { target: String }, + Permanent { header: String, target: String }, } impl Redirect { @@ -87,10 +87,15 @@ impl Redirect { // Getters + pub fn header(&self) -> &str { + match self { + Self::Permanent { header, .. } | Self::Temporary { header, .. } => header, + } + } + pub fn target(&self) -> &str { match self { - Self::Permanent { target } => target, - Self::Temporary { target } => target, + Self::Permanent { target, .. } | Self::Temporary { target, .. } => target, } } } @@ -124,9 +129,11 @@ impl std::str::FromStr for Redirect { match regex.get(1) { Some(code) => match code.as_str() { "0" => Ok(Self::Temporary { + header: header.to_string(), target: target(regex.get(2))?, }), "1" => Ok(Self::Permanent { + header: header.to_string(), target: target(regex.get(2))?, }), _ => todo!(), diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index e5ad6f4..f862fd9 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -4,7 +4,7 @@ pub use error::Error; const DEFAULT: (u8, &str) = (20, "Success"); pub enum Success { - Default { mime: String }, + Default { header: String, mime: String }, // reserved for 2* codes } @@ -30,9 +30,16 @@ impl Success { // Getters + pub fn header(&self) -> &str { + match self { + Self::Default { header, .. } => header, + } + .as_str() + } + pub fn mime(&self) -> &str { match self { - Self::Default { mime } => mime, + Self::Default { mime, .. } => mime, } } } @@ -53,7 +60,6 @@ impl std::str::FromStr for Success { type Err = Error; fn from_str(header: &str) -> Result { use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; - match Regex::split_simple( r"^20\s([^\/]+\/[^\s;]+)", header, @@ -68,6 +74,7 @@ impl std::str::FromStr for Success { Err(Error::Mime) } else { Ok(Self::Default { + header: header.to_string(), mime: mime.to_lowercase(), }) } From 376473660f12d2e274a0f058df749c90c59f59ca Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 01:17:14 +0200 Subject: [PATCH 342/392] update minor version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 54dd4b6..442d0ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.3" +version = "0.18.0" edition = "2024" license = "MIT" readme = "README.md" From b62f990bf22261fe38aaba88fa4f646169431a34 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 03:12:43 +0200 Subject: [PATCH 343/392] fix codes, validate header len --- src/client/connection/response/certificate.rs | 10 +++++++--- src/client/connection/response/certificate/error.rs | 10 +++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index e9f76c7..d78c133 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,9 +1,9 @@ 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"); +const REQUIRED: (u8, &str) = (60, "Certificate required"); +const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); +const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); /// 6* status code group /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates @@ -85,6 +85,10 @@ impl std::fmt::Display for Certificate { impl std::str::FromStr for Certificate { type Err = Error; fn from_str(header: &str) -> Result { + let len = header.len(); + if len > super::HEADER_LEN { + return Err(Error::HeaderLen(len)); + } if let Some(postfix) = header.strip_prefix("60") { return Ok(Self::Required { header: header.to_string(), diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs index 5cf1cf6..62f17d1 100644 --- a/src/client/connection/response/certificate/error.rs +++ b/src/client/connection/response/certificate/error.rs @@ -6,6 +6,7 @@ use std::{ #[derive(Debug)] pub enum Error { Code, + HeaderLen(usize), Utf8Error(Utf8Error), } @@ -13,7 +14,14 @@ impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { Self::Code => { - write!(f, "Status code error") + write!(f, "Unexpected status code") + } + Self::HeaderLen(l) => { + write!( + f, + "Header length reached protocol limit ({l} of {} bytes max)", + super::super::HEADER_LEN + ) } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") From 6dbf49cea3d4fe5d1caf73cf9c55e3941941c706 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 03:13:37 +0200 Subject: [PATCH 344/392] validate header len --- src/client/connection/response/success.rs | 47 +++++++++++-------- .../connection/response/success/error.rs | 24 ++++++---- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index f862fd9..fa4c236 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -60,26 +60,35 @@ impl std::str::FromStr for Success { type Err = Error; fn from_str(header: &str) -> Result { 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 { - header: header.to_string(), - mime: mime.to_lowercase(), - }) + + if header.len() > super::HEADER_LEN { + return Err(Error::HeaderLen(header.len())); + } + + // * keep separator after code as expected by protocol + match header.strip_prefix("20") { + Some(postfix) => match Regex::split_simple( + r"^\s+([^\/]+\/[^\s;]+)", + postfix, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ) + .get(1) + { + Some(mime) => { + let mime = mime.trim(); + if mime.is_empty() { + Err(Error::ContentType) + } else { + Ok(Self::Default { + header: header.to_string(), + mime: mime.to_lowercase(), + }) + } } - } - None => Err(Error::Protocol), + None => Err(Error::ContentType), + }, + None => Err(Error::Code), } } } diff --git a/src/client/connection/response/success/error.rs b/src/client/connection/response/success/error.rs index 2dbe363..7eaf2e0 100644 --- a/src/client/connection/response/success/error.rs +++ b/src/client/connection/response/success/error.rs @@ -5,23 +5,31 @@ use std::{ #[derive(Debug)] pub enum Error { - Protocol, - Mime, + Code, + ContentType, + HeaderLen(usize), Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::ContentType => { + write!(f, "Content type required") + } + Self::HeaderLen(l) => { + write!( + f, + "Header length reached protocol limit ({l} of {} bytes max)", + super::super::HEADER_LEN + ) + } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Mime => { - write!(f, "MIME error") - } } } } From ab8eb402a87f8131a63530f2e4f1eb55e5ddd9b9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 03:23:20 +0200 Subject: [PATCH 345/392] decode header bytes only --- src/client/connection/response/certificate.rs | 9 ++++- .../connection/response/failure/permanent.rs | 9 ++++- .../connection/response/failure/temporary.rs | 9 ++++- src/client/connection/response/input.rs | 33 ++++++++++++------- src/client/connection/response/redirect.rs | 9 ++++- src/client/connection/response/success.rs | 9 ++++- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index d78c133..6b9e774 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -31,7 +31,14 @@ impl Certificate { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - match std::str::from_utf8(buffer) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::HEADER_LEN { + super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e69510c..e33ea13 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -42,7 +42,14 @@ impl Permanent { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - match std::str::from_utf8(buffer) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::super::HEADER_LEN { + super::super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index c9d8083..21fc2b4 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -42,7 +42,14 @@ impl Temporary { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - match std::str::from_utf8(buffer) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::super::HEADER_LEN { + super::super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index c524b9a..37aa99f 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -21,7 +21,14 @@ impl Input { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - match std::str::from_utf8(buffer) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::HEADER_LEN { + super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -69,17 +76,19 @@ impl std::fmt::Display for Input { impl std::str::FromStr for Input { type Err = Error; fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("10") { - return Ok(Self::Default { - header: header.to_string(), - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("11") { - return Ok(Self::Sensitive { - header: header.to_string(), - message: message(postfix), - }); + if header.len() <= super::HEADER_LEN { + if let Some(postfix) = header.strip_prefix("10") { + return Ok(Self::Default { + header: header.to_string(), + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("11") { + return Ok(Self::Sensitive { + header: header.to_string(), + message: message(postfix), + }); + } } Err(Error::Protocol) } diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index cc838df..b936944 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -19,7 +19,14 @@ impl Redirect { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - match std::str::from_utf8(buffer) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::HEADER_LEN { + super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index fa4c236..e9c240e 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -14,7 +14,14 @@ impl Success { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - match std::str::from_utf8(buffer) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::HEADER_LEN { + super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } From 3f968d87b19a386b4e980fb86514760e51568c69 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 03:25:55 +0200 Subject: [PATCH 346/392] update error enum --- src/client/connection/response/input.rs | 29 ++++++++++--------- src/client/connection/response/input/error.rs | 16 +++++++--- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 37aa99f..78aa3a1 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -76,21 +76,22 @@ impl std::fmt::Display for Input { impl std::str::FromStr for Input { type Err = Error; fn from_str(header: &str) -> Result { - if header.len() <= super::HEADER_LEN { - if let Some(postfix) = header.strip_prefix("10") { - return Ok(Self::Default { - header: header.to_string(), - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("11") { - return Ok(Self::Sensitive { - header: header.to_string(), - message: message(postfix), - }); - } + if header.len() > super::HEADER_LEN { + return Err(Error::HeaderLen(header.len())); } - Err(Error::Protocol) + if let Some(postfix) = header.strip_prefix("10") { + return Ok(Self::Default { + header: header.to_string(), + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("11") { + return Ok(Self::Sensitive { + header: header.to_string(), + message: message(postfix), + }); + } + Err(Error::Code) } } diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index ae589e8..62f17d1 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -5,19 +5,27 @@ use std::{ #[derive(Debug)] pub enum Error { - Protocol, + Code, + HeaderLen(usize), Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::HeaderLen(l) => { + write!( + f, + "Header length reached protocol limit ({l} of {} bytes max)", + super::super::HEADER_LEN + ) + } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } - Self::Protocol => { - write!(f, "Protocol error") - } } } } From 9eb21bb6a3887a70aaba8239df7e95b2b5765207 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 15:06:53 +0200 Subject: [PATCH 347/392] Revert "hold raw header string" This reverts commit 5bb52fbd8c5aec9596c02ac641f10606407f0cd2. --- Cargo.toml | 2 +- src/client/connection/response/certificate.rs | 52 +++------------ .../connection/response/certificate/error.rs | 10 +-- src/client/connection/response/failure.rs | 7 -- .../connection/response/failure/permanent.rs | 60 ++++------------- .../connection/response/failure/temporary.rs | 60 ++++------------- src/client/connection/response/input.rs | 36 ++-------- src/client/connection/response/input/error.rs | 16 ++--- src/client/connection/response/redirect.rs | 24 ++----- src/client/connection/response/success.rs | 65 ++++++------------- .../connection/response/success/error.rs | 24 +++---- 11 files changed, 78 insertions(+), 278 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 442d0ee..54dd4b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.18.0" +version = "0.17.3" edition = "2024" license = "MIT" readme = "README.md" diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 6b9e774..160e2f0 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,28 +1,19 @@ pub mod error; pub use error::Error; -const REQUIRED: (u8, &str) = (60, "Certificate required"); -const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); -const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); +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 { - header: String, - message: Option, - }, + Required { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - NotAuthorized { - header: String, - message: Option, - }, + NotAuthorized { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - NotValid { - header: String, - message: Option, - }, + NotValid { message: Option }, } impl Certificate { @@ -31,14 +22,7 @@ impl Certificate { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::HEADER_LEN { - super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -55,20 +39,11 @@ impl Certificate { .0 } - pub fn header(&self) -> &str { - match self { - Self::Required { header, .. } - | Self::NotAuthorized { header, .. } - | Self::NotValid { header, .. } => header, - } - .as_str() - } - pub fn message(&self) -> Option<&str> { match self { - Self::Required { message, .. } - | Self::NotAuthorized { message, .. } - | Self::NotValid { message, .. } => message, + Self::Required { message } => message, + Self::NotAuthorized { message } => message, + Self::NotValid { message } => message, } .as_deref() } @@ -92,25 +67,18 @@ impl std::fmt::Display for Certificate { impl std::str::FromStr for Certificate { type Err = Error; fn from_str(header: &str) -> Result { - let len = header.len(); - if len > super::HEADER_LEN { - return Err(Error::HeaderLen(len)); - } if let Some(postfix) = header.strip_prefix("60") { return Ok(Self::Required { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("61") { return Ok(Self::NotAuthorized { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("62") { return Ok(Self::NotValid { - header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs index 62f17d1..5cf1cf6 100644 --- a/src/client/connection/response/certificate/error.rs +++ b/src/client/connection/response/certificate/error.rs @@ -6,7 +6,6 @@ use std::{ #[derive(Debug)] pub enum Error { Code, - HeaderLen(usize), Utf8Error(Utf8Error), } @@ -14,14 +13,7 @@ impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { Self::Code => { - write!(f, "Unexpected status code") - } - Self::HeaderLen(l) => { - write!( - f, - "Header length reached protocol limit ({l} of {} bytes max)", - super::super::HEADER_LEN - ) + write!(f, "Status code error") } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 8e85e84..40c8abf 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -45,13 +45,6 @@ impl Failure { } } - pub fn header(&self) -> &str { - match self { - Self::Permanent(permanent) => permanent.header(), - Self::Temporary(temporary) => temporary.header(), - } - } - pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e33ea13..e2ab9e0 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -10,30 +10,15 @@ 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 { - header: String, - message: Option, - }, + Default { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found - NotFound { - header: String, - message: Option, - }, + NotFound { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone - Gone { - header: String, - message: Option, - }, + Gone { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused - ProxyRequestRefused { - header: String, - message: Option, - }, + ProxyRequestRefused { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request - BadRequest { - header: String, - message: Option, - }, + BadRequest { message: Option }, } impl Permanent { @@ -42,14 +27,7 @@ impl Permanent { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::super::HEADER_LEN { - super::super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -68,24 +46,13 @@ impl Permanent { .0 } - pub fn header(&self) -> &str { - match self { - Self::Default { header, .. } - | Self::NotFound { header, .. } - | Self::Gone { header, .. } - | Self::ProxyRequestRefused { header, .. } - | Self::BadRequest { header, .. } => header, - } - .as_str() - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message, .. } - | Self::NotFound { message, .. } - | Self::Gone { message, .. } - | Self::ProxyRequestRefused { message, .. } - | Self::BadRequest { message, .. } => message, + Self::Default { message } => message, + Self::NotFound { message } => message, + Self::Gone { message } => message, + Self::ProxyRequestRefused { message } => message, + Self::BadRequest { message } => message, } .as_deref() } @@ -113,31 +80,26 @@ impl std::str::FromStr for Permanent { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("50") { return Ok(Self::Default { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("51") { return Ok(Self::NotFound { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("52") { return Ok(Self::Gone { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("53") { return Ok(Self::ProxyRequestRefused { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("59") { return Ok(Self::BadRequest { - header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 21fc2b4..768bdcd 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -10,30 +10,15 @@ 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 { - header: String, - message: Option, - }, + Default { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable { - header: String, - message: Option, - }, + ServerUnavailable { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError { - header: String, - message: Option, - }, + CgiError { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError { - header: String, - message: Option, - }, + ProxyError { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown { - header: String, - message: Option, - }, + SlowDown { message: Option }, } impl Temporary { @@ -42,14 +27,7 @@ impl Temporary { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::super::HEADER_LEN { - super::super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -68,24 +46,13 @@ impl Temporary { .0 } - pub fn header(&self) -> &str { - match self { - Self::Default { header, .. } - | Self::ServerUnavailable { header, .. } - | Self::CgiError { header, .. } - | Self::ProxyError { header, .. } - | Self::SlowDown { header, .. } => header, - } - .as_str() - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message, .. } - | Self::ServerUnavailable { message, .. } - | Self::CgiError { message, .. } - | Self::ProxyError { message, .. } - | Self::SlowDown { message, .. } => message, + Self::Default { message } => message, + Self::ServerUnavailable { message } => message, + Self::CgiError { message } => message, + Self::ProxyError { message } => message, + Self::SlowDown { message } => message, } .as_deref() } @@ -113,31 +80,26 @@ impl std::str::FromStr for Temporary { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("40") { return Ok(Self::Default { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("41") { return Ok(Self::ServerUnavailable { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("42") { return Ok(Self::CgiError { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("43") { return Ok(Self::ProxyError { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("44") { return Ok(Self::SlowDown { - header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 78aa3a1..b62276b 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -5,14 +5,8 @@ const DEFAULT: (u8, &str) = (10, "Input"); const SENSITIVE: (u8, &str) = (11, "Sensitive input"); pub enum Input { - Default { - header: String, - message: Option, - }, - Sensitive { - header: String, - message: Option, - }, + Default { message: Option }, + Sensitive { message: Option }, } impl Input { @@ -21,14 +15,7 @@ impl Input { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::HEADER_LEN { - super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -44,16 +31,10 @@ impl Input { .0 } - pub fn header(&self) -> &str { - match self { - Self::Default { header, .. } | Self::Sensitive { header, .. } => header, - } - .as_str() - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message, .. } | Self::Sensitive { message, .. } => message, + Self::Default { message } => message, + Self::Sensitive { message } => message, } .as_deref() } @@ -76,22 +57,17 @@ impl std::fmt::Display for Input { impl std::str::FromStr for Input { type Err = Error; fn from_str(header: &str) -> Result { - if header.len() > super::HEADER_LEN { - return Err(Error::HeaderLen(header.len())); - } if let Some(postfix) = header.strip_prefix("10") { return Ok(Self::Default { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("11") { return Ok(Self::Sensitive { - header: header.to_string(), message: message(postfix), }); } - Err(Error::Code) + Err(Error::Protocol) } } diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index 62f17d1..ae589e8 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -5,27 +5,19 @@ use std::{ #[derive(Debug)] pub enum Error { - Code, - HeaderLen(usize), + Protocol, Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Unexpected status code") - } - Self::HeaderLen(l) => { - write!( - f, - "Header length reached protocol limit ({l} of {} bytes max)", - super::super::HEADER_LEN - ) - } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } + Self::Protocol => { + write!(f, "Protocol error") + } } } } diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index b936944..2554fdf 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -8,9 +8,9 @@ const PERMANENT: (u8, &str) = (31, "Permanent redirect"); pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - Temporary { header: String, target: String }, + Temporary { target: String }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Permanent { header: String, target: String }, + Permanent { target: String }, } impl Redirect { @@ -19,14 +19,7 @@ impl Redirect { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::HEADER_LEN { - super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -94,15 +87,10 @@ impl Redirect { // Getters - pub fn header(&self) -> &str { - match self { - Self::Permanent { header, .. } | Self::Temporary { header, .. } => header, - } - } - pub fn target(&self) -> &str { match self { - Self::Permanent { target, .. } | Self::Temporary { target, .. } => target, + Self::Permanent { target } => target, + Self::Temporary { target } => target, } } } @@ -136,11 +124,9 @@ impl std::str::FromStr for Redirect { match regex.get(1) { Some(code) => match code.as_str() { "0" => Ok(Self::Temporary { - header: header.to_string(), target: target(regex.get(2))?, }), "1" => Ok(Self::Permanent { - header: header.to_string(), target: target(regex.get(2))?, }), _ => todo!(), diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index e9c240e..e5ad6f4 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -4,7 +4,7 @@ pub use error::Error; const DEFAULT: (u8, &str) = (20, "Success"); pub enum Success { - Default { header: String, mime: String }, + Default { mime: String }, // reserved for 2* codes } @@ -14,14 +14,7 @@ impl Success { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::HEADER_LEN { - super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -37,16 +30,9 @@ impl Success { // Getters - pub fn header(&self) -> &str { - match self { - Self::Default { header, .. } => header, - } - .as_str() - } - pub fn mime(&self) -> &str { match self { - Self::Default { mime, .. } => mime, + Self::Default { mime } => mime, } } } @@ -68,34 +54,25 @@ impl std::str::FromStr for Success { fn from_str(header: &str) -> Result { use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; - if header.len() > super::HEADER_LEN { - return Err(Error::HeaderLen(header.len())); - } - - // * keep separator after code as expected by protocol - match header.strip_prefix("20") { - Some(postfix) => match Regex::split_simple( - r"^\s+([^\/]+\/[^\s;]+)", - postfix, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ) - .get(1) - { - Some(mime) => { - let mime = mime.trim(); - if mime.is_empty() { - Err(Error::ContentType) - } else { - Ok(Self::Default { - header: header.to_string(), - mime: mime.to_lowercase(), - }) - } + 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::ContentType), - }, - None => Err(Error::Code), + } + None => Err(Error::Protocol), } } } diff --git a/src/client/connection/response/success/error.rs b/src/client/connection/response/success/error.rs index 7eaf2e0..2dbe363 100644 --- a/src/client/connection/response/success/error.rs +++ b/src/client/connection/response/success/error.rs @@ -5,31 +5,23 @@ use std::{ #[derive(Debug)] pub enum Error { - Code, - ContentType, - HeaderLen(usize), + Protocol, + Mime, Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Unexpected status code") - } - Self::ContentType => { - write!(f, "Content type required") - } - Self::HeaderLen(l) => { - write!( - f, - "Header length reached protocol limit ({l} of {} bytes max)", - super::super::HEADER_LEN - ) - } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Mime => { + write!(f, "MIME error") + } } } } From 2102d8887afeedbf6156cd8b6a89bfed12bcf0e4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 15:07:22 +0200 Subject: [PATCH 348/392] fix codes --- src/client/connection/response/certificate.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 160e2f0..423d0e8 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,9 +1,9 @@ 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"); +const REQUIRED: (u8, &str) = (60, "Certificate required"); +const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); +const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); /// 6* status code group /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates From a12a73d31175f9f34d51dc06aa1b8ec2684ac359 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 22 Mar 2025 19:03:42 +0200 Subject: [PATCH 349/392] hold `NetworkAddress` and `SocketConnection` as the `Connection` members --- src/client.rs | 2 +- src/client/connection.rs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index be66b5e..cff557e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -74,8 +74,8 @@ impl Client { Ok(socket_connection) => { match Connection::build( socket_connection, + network_address, certificate, - Some(network_address), is_session_resumption, ) { Ok(connection) => connection.request_async( diff --git a/src/client/connection.rs b/src/client/connection.rs index 267077c..028e00e 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -18,6 +18,8 @@ use glib::{ }; pub struct Connection { + pub network_address: NetworkAddress, + pub socket_connection: SocketConnection, pub tls_client_connection: TlsClientConnection, } @@ -27,24 +29,26 @@ impl Connection { /// Create new `Self` pub fn build( socket_connection: SocketConnection, + network_address: NetworkAddress, certificate: Option, - server_identity: Option, is_session_resumption: bool, ) -> Result { Ok(Self { tls_client_connection: match new_tls_client_connection( &socket_connection, - server_identity.as_ref(), + Some(&network_address), is_session_resumption, ) { Ok(tls_client_connection) => { - if let Some(ref certificate) = certificate { - tls_client_connection.set_certificate(certificate); + if let Some(ref c) = certificate { + tls_client_connection.set_certificate(c); } tls_client_connection } Err(e) => return Err(e), }, + network_address, + socket_connection, }) } From 7c518cecf6be56d8b917b10c5131b0ef7dcf3377 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 06:50:08 +0200 Subject: [PATCH 350/392] begin header holder implementation with lazy parser by getters, add request::Mode, add common header_bytes helper --- Cargo.toml | 2 +- README.md | 3 +- src/client/connection.rs | 58 +++++++------ src/client/connection/request.rs | 16 +++- src/client/connection/request/mode.rs | 4 + src/client/connection/response.rs | 78 ++++++++++------- src/client/connection/response/error.rs | 29 +++++-- src/client/connection/response/success.rs | 86 ++++--------------- .../connection/response/success/default.rs | 27 ++++++ .../response/success/default/error.rs | 20 +++++ .../response/success/default/header.rs | 43 ++++++++++ .../response/success/default/header/error.rs | 31 +++++++ .../connection/response/success/error.rs | 21 ++--- 13 files changed, 267 insertions(+), 151 deletions(-) create mode 100644 src/client/connection/request/mode.rs create mode 100644 src/client/connection/response/success/default.rs create mode 100644 src/client/connection/response/success/default/error.rs create mode 100644 src/client/connection/response/success/default/header.rs create mode 100644 src/client/connection/response/success/default/header/error.rs diff --git a/Cargo.toml b/Cargo.toml index 54dd4b6..442d0ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.3" +version = "0.18.0" edition = "2024" license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index cd21d88..c9e3c23 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ use gio::*; use glib::*; use ggemini::client::{ - connection::{Request, Response}, + connection::{request::{Mode, Request}, Response}, Client, }; @@ -51,6 +51,7 @@ fn main() -> ExitCode { Client::new().request_async( Request::Gemini { // or `Request::Titan` uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), + mode: Mode::Header // handle content separately (based on MIME) }, Priority::DEFAULT, Cancellable::new(), diff --git a/src/client/connection.rs b/src/client/connection.rs index 028e00e..8bea8b2 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -3,7 +3,7 @@ pub mod request; pub mod response; pub use error::Error; -pub use request::Request; +pub use request::{Mode, Request}; pub use response::Response; // Local dependencies @@ -74,36 +74,42 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match request { - Request::Gemini { .. } => Response::from_connection_async( - self, - priority, - cancellable, - |result, connection| { - callback(match result { - Ok(response) => Ok((response, connection)), - Err(e) => Err(Error::Response(e)), - }) - }, - ), + Request::Gemini { mode, .. } => match mode { + Mode::All => todo!(), + Mode::Header => Response::header_from_connection_async( + self, + priority, + cancellable, + |result, connection| { + callback(match result { + Ok(response) => Ok((response, connection)), + Err(e) => Err(Error::Response(e)), + }) + }, + ), + }, // Make sure **all data bytes** sent to the destination // > 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 - Request::Titan { data, .. } => output_stream.write_all_async( + Request::Titan { data, mode, .. } => output_stream.write_all_async( data, priority, Some(&cancellable.clone()), move |result| match result { - Ok(_) => Response::from_connection_async( - self, - priority, - cancellable, - |result, connection| { - callback(match result { - Ok(response) => Ok((response, connection)), - Err(e) => Err(Error::Response(e)), - }) - }, - ), + Ok(_) => match mode { + Mode::All => todo!(), + Mode::Header => Response::header_from_connection_async( + self, + priority, + cancellable, + |result, connection| { + callback(match result { + Ok(response) => Ok((response, connection)), + Err(e) => Err(Error::Response(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) /// 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) -pub fn new_tls_client_connection( +fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, is_session_resumption: bool, diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 75a4927..83632ca 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -1,5 +1,8 @@ pub mod error; +pub mod mode; + pub use error::Error; +pub use mode::Mode; // Local dependencies @@ -10,6 +13,7 @@ use glib::{Bytes, Uri, UriHideFlags}; pub enum Request { Gemini { uri: Uri, + mode: Mode, }, Titan { uri: Uri, @@ -18,6 +22,7 @@ pub enum Request { /// but server MAY reject the request without `mime` value provided. mime: Option, token: Option, + mode: Mode, }, } @@ -27,12 +32,13 @@ impl Request { /// Generate header string for `Self` pub fn header(&self) -> String { match self { - Self::Gemini { uri } => format!("{uri}\r\n"), + Self::Gemini { uri, .. } => format!("{uri}\r\n"), Self::Titan { uri, data, mime, token, + .. } => { let mut header = format!( "{};size={}", @@ -57,7 +63,7 @@ impl Request { /// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html) pub fn uri(&self) -> &Uri { match self { - Self::Gemini { uri } => uri, + Self::Gemini { uri, .. } => uri, Self::Titan { uri, .. } => uri, } } @@ -79,7 +85,8 @@ fn test_gemini_header() { assert_eq!( Request::Gemini { - uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() + uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(), + mode: Mode::Header } .header(), format!("{REQUEST}\r\n") @@ -103,7 +110,8 @@ fn test_titan_header() { .unwrap(), data: Bytes::from(DATA), mime: Some(MIME.to_string()), - token: Some(TOKEN.to_string()) + token: Some(TOKEN.to_string()), + mode: Mode::Header } .header(), format!( diff --git a/src/client/connection/request/mode.rs b/src/client/connection/request/mode.rs new file mode 100644 index 0000000..6713bbd --- /dev/null +++ b/src/client/connection/request/mode.rs @@ -0,0 +1,4 @@ +pub enum Mode { + Header, + All, +} diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 9a16679..98fe2e3 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -6,7 +6,7 @@ pub mod redirect; pub mod success; pub use certificate::Certificate; -pub use error::Error; +pub use error::{Error, HeaderBytesError}; pub use failure::Failure; pub use input::Input; pub use redirect::Redirect; @@ -29,13 +29,13 @@ pub enum Response { impl Response { /// Asynchronously create new `Self` for given `Connection` - pub fn from_connection_async( + pub fn header_from_connection_async( connection: Connection, priority: Priority, cancellable: Cancellable, callback: impl FnOnce(Result, Connection) + 'static, ) { - from_stream_async( + header_from_stream_async( Vec::with_capacity(HEADER_LEN), connection.stream(), cancellable, @@ -44,12 +44,12 @@ impl Response { callback( match result { Ok(buffer) => match buffer.first() { - Some(byte) => match byte { + Some(b) => match b { b'1' => match Input::from_utf8(&buffer) { Ok(input) => Ok(Self::Input(input)), 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)), Err(e) => Err(Error::Success(e)), }, @@ -65,9 +65,9 @@ impl Response { Ok(certificate) => Ok(Self::Certificate(certificate)), 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), }, @@ -84,43 +84,63 @@ impl Response { /// /// Return UTF-8 buffer collected /// * requires `IOStream` reference to keep `Connection` active in async thread -fn from_stream_async( +fn header_from_stream_async( mut buffer: Vec, stream: impl IsA, cancellable: Cancellable, priority: Priority, - on_complete: impl FnOnce(Result, Error>) + 'static, + callback: impl FnOnce(Result, 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)); + Ok((bytes, size)) => { + if size == 0 { + return callback(Ok(buffer)); } - - // Read next byte without record - if bytes.contains(&b'\r') { - return from_stream_async(buffer, stream, cancellable, priority, on_complete); + if buffer.len() + bytes.len() > HEADER_LEN { + buffer.extend(bytes); + return callback(Err(Error::Protocol(buffer))); } - - // Complete without record - if bytes.contains(&b'\n') { - return on_complete(Ok(buffer)); + if bytes[0] == b'\r' { + buffer.extend(bytes); + return header_from_stream_async( + buffer, + stream, + cancellable, + priority, + callback, + ); } - - // Record - buffer.append(&mut bytes); - - // Continue - from_stream_async(buffer, stream, cancellable, priority, on_complete); + if bytes[0] == b'\n' { + buffer.extend(bytes); + return callback(Ok(buffer)); + } + buffer.extend(bytes); + 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) +} diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index df8cda4..022ed62 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -6,10 +6,10 @@ use std::{ #[derive(Debug)] pub enum Error { Certificate(super::certificate::Error), - Code, + Code(u8), Failure(super::failure::Error), Input(super::input::Error), - Protocol, + Protocol(Vec), Redirect(super::redirect::Error), Stream(glib::Error, Vec), Success(super::success::Error), @@ -22,8 +22,8 @@ impl Display for Error { Self::Certificate(e) => { write!(f, "Certificate error: {e}") } - Self::Code => { - write!(f, "Code group error") + Self::Code(b) => { + write!(f, "Unexpected status code byte: {b}") } Self::Failure(e) => { write!(f, "Failure error: {e}") @@ -31,7 +31,7 @@ impl Display for Error { Self::Input(e) => { write!(f, "Input error: {e}") } - Self::Protocol => { + Self::Protocol(..) => { write!(f, "Protocol error") } 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") + } + } + } +} diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index e5ad6f4..591510d 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -1,89 +1,33 @@ +pub mod default; pub mod error; + +pub use default::Default; pub use error::Error; -const DEFAULT: (u8, &str) = (20, "Success"); +pub const CODE: u8 = b'2'; pub enum Success { - Default { mime: String }, + Default(Default), // reserved for 2* codes } impl Success { // Constructors - /// Create new `Self` from buffer include header bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + /// Parse new `Self` from buffer bytes + pub fn parse(buffer: &[u8]) -> Result { + if !buffer.first().is_some_and(|b| *b == CODE) { + return Err(Error::Code); } - } - - // 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 { - 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), + match Default::parse(&buffer) { + Ok(default) => Ok(Self::Default(default)), + Err(e) => Err(Error::Default(e)), } } } #[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); +fn test() { + // let default = Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()); + todo!() } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs new file mode 100644 index 0000000..34f9cb0 --- /dev/null +++ b/src/client/connection/response/success/default.rs @@ -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>, +} + +impl Default { + // Constructors + + pub fn parse(buffer: &[u8]) -> Result { + 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, + }) + } +} diff --git a/src/client/connection/response/success/default/error.rs b/src/client/connection/response/success/default/error.rs new file mode 100644 index 0000000..d5b28b5 --- /dev/null +++ b/src/client/connection/response/success/default/error.rs @@ -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}") + } + } + } +} diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs new file mode 100644 index 0000000..ef326bc --- /dev/null +++ b/src/client/connection/response/success/default/header.rs @@ -0,0 +1,43 @@ +pub mod error; +pub use error::Error; + +pub struct Header(Vec); + +impl Header { + // Constructors + + pub fn parse(buffer: &[u8]) -> Result { + 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 { + 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 + } +} diff --git a/src/client/connection/response/success/default/header/error.rs b/src/client/connection/response/success/default/header/error.rs new file mode 100644 index 0000000..4daca3a --- /dev/null +++ b/src/client/connection/response/success/default/header/error.rs @@ -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}") + } + } + } +} diff --git a/src/client/connection/response/success/error.rs b/src/client/connection/response/success/error.rs index 2dbe363..fe32c5f 100644 --- a/src/client/connection/response/success/error.rs +++ b/src/client/connection/response/success/error.rs @@ -1,26 +1,19 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Protocol, - Mime, - Utf8Error(Utf8Error), + Code, + Default(super::default::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Code => { + write!(f, "Unexpected status code") } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Mime => { - write!(f, "MIME error") + Self::Default(e) => { + write!(f, "Header error: {e}") } } } From 0717e473b7d6707fdeefcc8c476b2e0670e49dc6 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:06:54 +0200 Subject: [PATCH 351/392] remove unsupported modes, add comments --- src/client/connection.rs | 2 -- src/client/connection/request/mode.rs | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 8bea8b2..c62a874 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -75,7 +75,6 @@ impl Connection { move |result| match result { Ok(_) => match request { Request::Gemini { mode, .. } => match mode { - Mode::All => todo!(), Mode::Header => Response::header_from_connection_async( self, priority, @@ -97,7 +96,6 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match mode { - Mode::All => todo!(), Mode::Header => Response::header_from_connection_async( self, priority, diff --git a/src/client/connection/request/mode.rs b/src/client/connection/request/mode.rs index 6713bbd..0d006ef 100644 --- a/src/client/connection/request/mode.rs +++ b/src/client/connection/request/mode.rs @@ -1,4 +1,6 @@ +/// Request modes pub enum Mode { + /// Request header bytes only, process content bytes manually + /// * useful for manual content type handle: text, stream or large content loaded by chunks Header, - All, } From 68e789412597f94e81aaa882cf0cd81b2608167a Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:10:22 +0200 Subject: [PATCH 352/392] apply clippy --- src/client/connection/response/success.rs | 4 ++-- src/client/connection/response/success/default.rs | 2 +- src/client/connection/response/success/default/header.rs | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index 591510d..6ead866 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -16,10 +16,10 @@ impl Success { /// Parse new `Self` from buffer bytes pub fn parse(buffer: &[u8]) -> Result { - if !buffer.first().is_some_and(|b| *b == CODE) { + if buffer.first().is_none_or(|b| *b != CODE) { return Err(Error::Code); } - match Default::parse(&buffer) { + match Default::parse(buffer) { Ok(default) => Ok(Self::Default(default)), Err(e) => Err(Error::Default(e)), } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 34f9cb0..90ac8f5 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -18,7 +18,7 @@ impl Default { if !buffer.starts_with(CODE) { return Err(Error::Code); } - let header = Header::parse(buffer).map_err(|e| Error::Header(e))?; + let header = Header::parse(buffer).map_err(Error::Header)?; Ok(Self { content: buffer.get(header.len() + 1..).map(|v| v.to_vec()), header, diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs index ef326bc..306a984 100644 --- a/src/client/connection/response/success/default/header.rs +++ b/src/client/connection/response/success/default/header.rs @@ -12,7 +12,7 @@ impl Header { } Ok(Self( crate::client::connection::response::header_bytes(buffer) - .map_err(|e| Error::Header(e))? + .map_err(Error::Header)? .to_vec(), )) } @@ -23,7 +23,7 @@ impl Header { pub fn mime(&self) -> Result { glib::Regex::split_simple( r"^\d{2}\s([^\/]+\/[^\s;]+)", - std::str::from_utf8(&self.0).map_err(|e| Error::Utf8Error(e))?, + std::str::from_utf8(&self.0).map_err(Error::Utf8Error)?, glib::RegexCompileFlags::DEFAULT, glib::RegexMatchFlags::DEFAULT, ) @@ -37,6 +37,10 @@ impl Header { self.0.len() } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn as_bytes(&self) -> &[u8] { &self.0 } From 3de096ccedc215408979f2efb0b7ce984f574233 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:35:57 +0200 Subject: [PATCH 353/392] add tests --- src/client/connection/response/success.rs | 9 +++++++-- src/client/connection/response/success/default.rs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index 6ead866..6316864 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -28,6 +28,11 @@ impl Success { #[test] fn test() { - // let default = Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()); - todo!() + match Success::parse(format!("20 text/gemini; charset=utf-8; lang=en\r\n").as_bytes()).unwrap() + { + Success::Default(default) => { + assert_eq!(default.header.mime().unwrap(), "text/gemini"); + assert_eq!(default.content, None) + } + } } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 90ac8f5..5d629b8 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -20,8 +20,19 @@ impl Default { } let header = Header::parse(buffer).map_err(Error::Header)?; Ok(Self { - content: buffer.get(header.len() + 1..).map(|v| v.to_vec()), + content: buffer + .get(header.len() + 1..) + .filter(|s| !s.is_empty()) + .map(|v| v.to_vec()), header, }) } } + +#[test] +fn test() { + let default = + Default::parse(format!("20 text/gemini; charset=utf-8; lang=en\r\n").as_bytes()).unwrap(); + assert_eq!(default.header.mime().unwrap(), "text/gemini"); + assert_eq!(default.content, None) +} From 71043bbf7375c9384ce239e629d9897d65094b47 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:38:26 +0200 Subject: [PATCH 354/392] remove extra format --- src/client/connection/response/success.rs | 3 +-- src/client/connection/response/success/default.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index 6316864..a46e141 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -28,8 +28,7 @@ impl Success { #[test] fn test() { - match Success::parse(format!("20 text/gemini; charset=utf-8; lang=en\r\n").as_bytes()).unwrap() - { + match Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { Success::Default(default) => { assert_eq!(default.header.mime().unwrap(), "text/gemini"); assert_eq!(default.content, None) diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 5d629b8..ae57dca 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -31,8 +31,7 @@ impl Default { #[test] fn test() { - let default = - Default::parse(format!("20 text/gemini; charset=utf-8; lang=en\r\n").as_bytes()).unwrap(); + let default = Default::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); assert_eq!(default.header.mime().unwrap(), "text/gemini"); assert_eq!(default.content, None) } From 68277f8e8305ad7eb85c38476e7c9c6965b18468 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:43:44 +0200 Subject: [PATCH 355/392] update example --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c9e3c23..622e680 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,11 @@ fn main() -> ExitCode { None, // optional `GTlsCertificate` |result| match result { Ok((response, _connection)) => match response { - Response::Success(success) => match success.mime() { - "text/gemini" => todo!(), - _ => todo!(), + Response::Success(success) => match success { + Success::Default(default) => match default.header.mime().unwrap().as_str() { + "text/gemini" => todo!(), + _ => todo!(), + } }, _ => todo!(), }, From 5360c6bf19337ed74493b451b96ad002e1488a0f Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 18:31:55 +0200 Subject: [PATCH 356/392] close code members --- src/client/connection/response/success.rs | 2 +- src/client/connection/response/success/default.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index a46e141..2139950 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -4,7 +4,7 @@ pub mod error; pub use default::Default; pub use error::Error; -pub const CODE: u8 = b'2'; +const CODE: u8 = b'2'; pub enum Success { Default(Default), diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index ae57dca..7544723 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -4,7 +4,7 @@ pub mod header; pub use error::Error; pub use header::Header; -pub const CODE: &[u8] = b"20"; +const CODE: &[u8] = b"20"; pub struct Default { pub header: Header, From 8feab6b93bc276685b160081a3b7a4948b384596 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 19:40:12 +0200 Subject: [PATCH 357/392] rename constructors --- src/client/connection/response.rs | 2 +- src/client/connection/response/success.rs | 6 +++--- src/client/connection/response/success/default.rs | 7 ++++--- src/client/connection/response/success/default/header.rs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 98fe2e3..a92e6e3 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -49,7 +49,7 @@ impl Response { Ok(input) => Ok(Self::Input(input)), Err(e) => Err(Error::Input(e)), }, - b'2' => match Success::parse(&buffer) { + b'2' => match Success::from_utf8(&buffer) { Ok(success) => Ok(Self::Success(success)), Err(e) => Err(Error::Success(e)), }, diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index 2139950..ecee769 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -15,11 +15,11 @@ impl Success { // Constructors /// Parse new `Self` from buffer bytes - pub fn parse(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { if buffer.first().is_none_or(|b| *b != CODE) { return Err(Error::Code); } - match Default::parse(buffer) { + match Default::from_utf8(buffer) { Ok(default) => Ok(Self::Default(default)), Err(e) => Err(Error::Default(e)), } @@ -28,7 +28,7 @@ impl Success { #[test] fn test() { - match Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { + match Success::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { Success::Default(default) => { assert_eq!(default.header.mime().unwrap(), "text/gemini"); assert_eq!(default.content, None) diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 7544723..6180f2e 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -14,11 +14,11 @@ pub struct Default { impl Default { // Constructors - pub fn parse(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(CODE) { return Err(Error::Code); } - let header = Header::parse(buffer).map_err(Error::Header)?; + let header = Header::from_utf8(buffer).map_err(Error::Header)?; Ok(Self { content: buffer .get(header.len() + 1..) @@ -31,7 +31,8 @@ impl Default { #[test] fn test() { - let default = Default::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); + let default = + Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); assert_eq!(default.header.mime().unwrap(), "text/gemini"); assert_eq!(default.content, None) } diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs index 306a984..929cb96 100644 --- a/src/client/connection/response/success/default/header.rs +++ b/src/client/connection/response/success/default/header.rs @@ -6,7 +6,7 @@ pub struct Header(Vec); impl Header { // Constructors - pub fn parse(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(super::CODE) { return Err(Error::Code); } From 161142c8090f0a3a836ecff4710468d0eff928a3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 19:57:54 +0200 Subject: [PATCH 358/392] rename mode const --- README.md | 2 +- src/client/connection.rs | 4 ++-- src/client/connection/request.rs | 4 ++-- src/client/connection/request/mode.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 622e680..3abf80f 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ fn main() -> ExitCode { Client::new().request_async( Request::Gemini { // or `Request::Titan` uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), - mode: Mode::Header // handle content separately (based on MIME) + mode: Mode::HeaderOnly // handle content separately (based on MIME) }, Priority::DEFAULT, Cancellable::new(), diff --git a/src/client/connection.rs b/src/client/connection.rs index c62a874..d1cd849 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -75,7 +75,7 @@ impl Connection { move |result| match result { Ok(_) => match request { Request::Gemini { mode, .. } => match mode { - Mode::Header => Response::header_from_connection_async( + Mode::HeaderOnly => Response::header_from_connection_async( self, priority, cancellable, @@ -96,7 +96,7 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match mode { - Mode::Header => Response::header_from_connection_async( + Mode::HeaderOnly => Response::header_from_connection_async( self, priority, cancellable, diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 83632ca..f67238b 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -86,7 +86,7 @@ fn test_gemini_header() { assert_eq!( Request::Gemini { uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(), - mode: Mode::Header + mode: Mode::HeaderOnly } .header(), format!("{REQUEST}\r\n") @@ -111,7 +111,7 @@ fn test_titan_header() { data: Bytes::from(DATA), mime: Some(MIME.to_string()), token: Some(TOKEN.to_string()), - mode: Mode::Header + mode: Mode::HeaderOnly } .header(), format!( diff --git a/src/client/connection/request/mode.rs b/src/client/connection/request/mode.rs index 0d006ef..b1d8a67 100644 --- a/src/client/connection/request/mode.rs +++ b/src/client/connection/request/mode.rs @@ -2,5 +2,5 @@ pub enum Mode { /// Request header bytes only, process content bytes manually /// * useful for manual content type handle: text, stream or large content loaded by chunks - Header, + HeaderOnly, } From a32eccf5cb4d50ac5373f8373838623cc772875e Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 20:46:54 +0200 Subject: [PATCH 359/392] reorganize input format: make constructors lazy, parse members on get --- src/client/connection/response/input.rs | 127 +++++++----------- .../connection/response/input/default.rs | 61 +++++++++ .../response/input/default/error.rs | 24 ++++ src/client/connection/response/input/error.rs | 31 ++++- .../connection/response/input/sensitive.rs | 61 +++++++++ .../response/input/sensitive/error.rs | 24 ++++ 6 files changed, 243 insertions(+), 85 deletions(-) create mode 100644 src/client/connection/response/input/default.rs create mode 100644 src/client/connection/response/input/default/error.rs create mode 100644 src/client/connection/response/input/sensitive.rs create mode 100644 src/client/connection/response/input/sensitive/error.rs diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index b62276b..81988ff 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -1,12 +1,17 @@ +pub mod default; pub mod error; +pub mod sensitive; + +pub use default::Default; pub use error::Error; +pub use sensitive::Sensitive; -const DEFAULT: (u8, &str) = (10, "Input"); -const SENSITIVE: (u8, &str) = (11, "Sensitive input"); +const CODE: u8 = b'1'; +/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected) pub enum Input { - Default { message: Option }, - Sensitive { message: Option }, + Default(Default), + Sensitive(Sensitive), } impl Input { @@ -14,97 +19,63 @@ impl Input { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::Sensitive( + Sensitive::from_utf8(buffer).map_err(Error::Sensitive)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // 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, + Self::Default(default) => default.message(), + Self::Sensitive(sensitive) => sensitive.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 { .. } => DEFAULT, - Self::Sensitive { .. } => SENSITIVE, - } - .1 - ) - } -} - -impl std::str::FromStr for Input { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("10") { - return Ok(Self::Default { - message: message(postfix), - }); + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::Sensitive(sensitive) => sensitive.as_str(), } - if let Some(postfix) = header.strip_prefix("11") { - return Ok(Self::Sensitive { - message: message(postfix), - }); - } - Err(Error::Protocol) } -} -// Tools - -fn message(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::Sensitive(sensitive) => sensitive.as_bytes(), + } } } #[test] -fn test_from_str() { - use std::str::FromStr; - +fn test() { + fn t(source: &str, message: Option<&str>) { + let bytes = source.as_bytes(); + let input = Input::from_utf8(bytes).unwrap(); + assert_eq!(input.message(), message); + assert_eq!(input.as_str(), source); + assert_eq!(input.as_bytes(), bytes); + } // 10 - let default = Input::from_str("10 Default\r\n").unwrap(); - assert_eq!(default.message(), Some("Default")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Input::from_str("10\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - + t("10 Default\r\n", Some("Default")); + t("10\r\n", None); // 11 - let sensitive = Input::from_str("11 Sensitive\r\n").unwrap(); - assert_eq!(sensitive.message(), Some("Sensitive")); - assert_eq!(sensitive.to_code(), SENSITIVE.0); - assert_eq!(sensitive.to_string(), SENSITIVE.1); - - let sensitive = Input::from_str("11\r\n").unwrap(); - assert_eq!(sensitive.message(), None); - assert_eq!(sensitive.to_code(), SENSITIVE.0); - assert_eq!(sensitive.to_string(), SENSITIVE.1); + t("11 Sensitive\r\n", Some("Sensitive")); + t("11\r\n", None); } diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs new file mode 100644 index 0000000..f0a04f3 --- /dev/null +++ b/src/client/connection/response/input/default.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"10"; + +/// Hold header `String` for [10](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let default = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); + assert_eq!(default.message(), Some("Default")); + assert_eq!(default.as_str(), "10 Default\r\n"); + + let default = Default::from_utf8("10\r\n".as_bytes()).unwrap(); + assert_eq!(default.message(), None); + assert_eq!(default.as_str(), "10\r\n"); + + // err + assert!(Default::from_utf8("12 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/input/default/error.rs b/src/client/connection/response/input/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/input/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index ae589e8..bcb7f8a 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -1,23 +1,40 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { + Default(super::default::Error), + FirstByte(u8), Protocol, - Utf8Error(Utf8Error), + SecondByte(u8), + Sensitive(super::sensitive::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") + } + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } Self::Protocol => { write!(f, "Protocol error") } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::Sensitive(e) => { + write!(f, "Sensitive parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") + } } } } diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs new file mode 100644 index 0000000..d456490 --- /dev/null +++ b/src/client/connection/response/input/sensitive.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"11"; + +/// Hold header `String` for [11](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Sensitive(String); + +impl Sensitive { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let sensitive = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); + assert_eq!(sensitive.message(), Some("Sensitive")); + assert_eq!(sensitive.as_str(), "11 Sensitive\r\n"); + + let sensitive = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); + assert_eq!(sensitive.message(), None); + assert_eq!(sensitive.as_str(), "11\r\n"); + + // err + assert!(Sensitive::from_utf8("12 Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/input/sensitive/error.rs b/src/client/connection/response/input/sensitive/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/input/sensitive/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From e94923ecb55b6e9e715562137dbf50b4e9870119 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 20:49:33 +0200 Subject: [PATCH 360/392] fix last byte inclusion --- src/client/connection/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index a92e6e3..840cf08 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -137,7 +137,7 @@ fn header_bytes(buffer: &[u8]) -> Result<&[u8], HeaderBytesError> { if *b == b'\r' { let n = i + 1; if buffer.get(n).is_some_and(|b| *b == b'\n') { - return Ok(&buffer[..n]); + return Ok(&buffer[..n + 1]); } break; } From 845f3dc77e3b348ad9e3778caf6c12eac2cae60b Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 22:36:00 +0200 Subject: [PATCH 361/392] enshort var names --- src/client/connection/response/input.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 81988ff..549b13e 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -66,11 +66,11 @@ impl Input { #[test] fn test() { fn t(source: &str, message: Option<&str>) { - let bytes = source.as_bytes(); - let input = Input::from_utf8(bytes).unwrap(); - assert_eq!(input.message(), message); - assert_eq!(input.as_str(), source); - assert_eq!(input.as_bytes(), bytes); + let b = source.as_bytes(); + let i = Input::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); } // 10 t("10 Default\r\n", Some("Default")); From 1b96270598c0d7b19564b2f8eb28782883e0c7c7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 22:38:07 +0200 Subject: [PATCH 362/392] remove deprecated enum values --- src/client/connection/response/input/error.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index bcb7f8a..6763727 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -4,7 +4,6 @@ use std::fmt::{Display, Formatter, Result}; pub enum Error { Default(super::default::Error), FirstByte(u8), - Protocol, SecondByte(u8), Sensitive(super::sensitive::Error), UndefinedFirstByte, @@ -20,9 +19,6 @@ impl Display for Error { Self::FirstByte(b) => { write!(f, "Unexpected first byte: {b}") } - Self::Protocol => { - write!(f, "Protocol error") - } Self::SecondByte(b) => { write!(f, "Unexpected second byte: {b}") } From 232531a0bc403668b70c0415596688404a91d973 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 22:50:03 +0200 Subject: [PATCH 363/392] reorganize certificate structs format: make constructors lazy, parse members on get --- src/client/connection/response/certificate.rs | 144 ++++++++---------- .../connection/response/certificate/error.rs | 37 +++-- .../response/certificate/not_authorized.rs | 61 ++++++++ .../certificate/not_authorized/error.rs | 24 +++ .../response/certificate/not_valid.rs | 61 ++++++++ .../response/certificate/not_valid/error.rs | 24 +++ .../response/certificate/required.rs | 61 ++++++++ .../response/certificate/required/error.rs | 24 +++ 8 files changed, 345 insertions(+), 91 deletions(-) create mode 100644 src/client/connection/response/certificate/not_authorized.rs create mode 100644 src/client/connection/response/certificate/not_authorized/error.rs create mode 100644 src/client/connection/response/certificate/not_valid.rs create mode 100644 src/client/connection/response/certificate/not_valid/error.rs create mode 100644 src/client/connection/response/certificate/required.rs create mode 100644 src/client/connection/response/certificate/required/error.rs diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 423d0e8..e67ded4 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,19 +1,24 @@ pub mod error; -pub use error::Error; +pub mod not_authorized; +pub mod not_valid; +pub mod required; -const REQUIRED: (u8, &str) = (60, "Certificate required"); -const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); -const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); +pub use error::Error; +pub use not_authorized::NotAuthorized; +pub use not_valid::NotValid; +pub use required::Required; + +const CODE: u8 = b'6'; /// 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 }, + Required(Required), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - NotAuthorized { message: Option }, + NotAuthorized(NotAuthorized), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - NotValid { message: Option }, + NotValid(NotValid), } impl Certificate { @@ -21,95 +26,72 @@ impl Certificate { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Required( + Required::from_utf8(buffer).map_err(Error::Required)?, + )), + b'1' => Ok(Self::NotAuthorized( + NotAuthorized::from_utf8(buffer).map_err(Error::NotAuthorized)?, + )), + b'2' => Ok(Self::NotValid( + NotValid::from_utf8(buffer).map_err(Error::NotValid)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // 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, + Self::Required(required) => required.message(), + Self::NotAuthorized(not_authorized) => not_authorized.message(), + Self::NotValid(not_valid) => not_valid.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 { .. } => REQUIRED, - Self::NotAuthorized { .. } => NOT_AUTHORIZED, - Self::NotValid { .. } => NOT_VALID, - } - .1 - ) + pub fn as_str(&self) -> &str { + match self { + Self::Required(required) => required.as_str(), + Self::NotAuthorized(not_authorized) => not_authorized.as_str(), + Self::NotValid(not_valid) => not_valid.as_str(), + } } -} -impl std::str::FromStr for Certificate { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("60") { - return Ok(Self::Required { - message: message(postfix), - }); + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Required(required) => required.as_bytes(), + Self::NotAuthorized(not_authorized) => not_authorized.as_bytes(), + Self::NotValid(not_valid) => not_valid.as_bytes(), } - 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 { - 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); - assert_eq!(required.to_string(), REQUIRED.1); - - 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); +fn test() { + fn t(source: &str, message: Option<&str>) { + let b = source.as_bytes(); + let c = Certificate::from_utf8(b).unwrap(); + assert_eq!(c.message(), message); + assert_eq!(c.as_str(), source); + assert_eq!(c.as_bytes(), b); + } + // 60 + t("60 Required\r\n", Some("Required")); + t("60\r\n", None); + // 61 + t("61 Not Authorized\r\n", Some("Not Authorized")); + t("61\r\n", None); + // 62 + t("61 Not Valid\r\n", Some("Not Valid")); + t("61\r\n", None); } diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs index 5cf1cf6..a710617 100644 --- a/src/client/connection/response/certificate/error.rs +++ b/src/client/connection/response/certificate/error.rs @@ -1,22 +1,39 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + FirstByte(u8), + NotAuthorized(super::not_authorized::Error), + NotValid(super::not_valid::Error), + Required(super::required::Error), + SecondByte(u8), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::NotAuthorized(e) => { + write!(f, "NotAuthorized status parse error: {e}") + } + Self::NotValid(e) => { + write!(f, "NotValid status parse error: {e}") + } + Self::Required(e) => { + write!(f, "Required status parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs new file mode 100644 index 0000000..fe85d1e --- /dev/null +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"61"; + +/// Hold header `String` for [61](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotAuthorized(String); + +impl NotAuthorized { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let not_authorized = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap(); + assert_eq!(not_authorized.message(), Some("Not Authorized")); + assert_eq!(not_authorized.as_str(), "61 Not Authorized\r\n"); + + let not_authorized = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap(); + assert_eq!(not_authorized.message(), None); + assert_eq!(not_authorized.as_str(), "61\r\n"); + + // err + assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/not_authorized/error.rs b/src/client/connection/response/certificate/not_authorized/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/not_authorized/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs new file mode 100644 index 0000000..35ad475 --- /dev/null +++ b/src/client/connection/response/certificate/not_valid.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"62"; + +/// Hold header `String` for [62](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotValid(String); + +impl NotValid { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let not_valid = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap(); + assert_eq!(not_valid.message(), Some("Not Valid")); + assert_eq!(not_valid.as_str(), "62 Not Valid\r\n"); + + let not_valid = NotValid::from_utf8("62\r\n".as_bytes()).unwrap(); + assert_eq!(not_valid.message(), None); + assert_eq!(not_valid.as_str(), "62\r\n"); + + // err + // @TODO assert!(NotValid::from_utf8("62Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("63 Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/not_valid/error.rs b/src/client/connection/response/certificate/not_valid/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/not_valid/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/certificate/required.rs b/src/client/connection/response/certificate/required.rs new file mode 100644 index 0000000..df0ef63 --- /dev/null +++ b/src/client/connection/response/certificate/required.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"60"; + +/// Hold header `String` for [60](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Required(String); + +impl Required { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let required = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap(); + assert_eq!(required.message(), Some("Required")); + assert_eq!(required.as_str(), "60 Required\r\n"); + + let required = Required::from_utf8("60\r\n".as_bytes()).unwrap(); + assert_eq!(required.message(), None); + assert_eq!(required.as_str(), "60\r\n"); + + // err + assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/required/error.rs b/src/client/connection/response/certificate/required/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/required/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From 4eb998ef20767da11c039360158fb11030fee954 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 22:51:55 +0200 Subject: [PATCH 364/392] draft potential test --- src/client/connection/response/input/default.rs | 4 ++-- src/client/connection/response/input/sensitive.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs index f0a04f3..8d24b62 100644 --- a/src/client/connection/response/input/default.rs +++ b/src/client/connection/response/input/default.rs @@ -54,8 +54,8 @@ fn test() { assert_eq!(default.as_str(), "10\r\n"); // err - assert!(Default::from_utf8("12 Fail\r\n".as_bytes()).is_err()); - assert!(Default::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + // @TODO assert!(Default::from_utf8("10Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("Fail".as_bytes()).is_err()); } diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs index d456490..cc6ab5d 100644 --- a/src/client/connection/response/input/sensitive.rs +++ b/src/client/connection/response/input/sensitive.rs @@ -54,8 +54,8 @@ fn test() { assert_eq!(sensitive.as_str(), "11\r\n"); // err - assert!(Sensitive::from_utf8("12 Fail\r\n".as_bytes()).is_err()); - assert!(Sensitive::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + // @TODO assert!(Sensitive::from_utf8("11Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err()); } From 473ed48715385fc88845d9081416e4e026eddf98 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 23:32:18 +0200 Subject: [PATCH 365/392] make final codes public, add comments --- .../connection/response/certificate/not_authorized.rs | 5 +++-- src/client/connection/response/certificate/not_valid.rs | 5 +++-- src/client/connection/response/certificate/required.rs | 5 +++-- src/client/connection/response/input/default.rs | 5 +++-- src/client/connection/response/input/sensitive.rs | 5 +++-- src/client/connection/response/success/default.rs | 6 +++++- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs index fe85d1e..980d9ac 100644 --- a/src/client/connection/response/certificate/not_authorized.rs +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"61"; +/// [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code +pub const CODE: &[u8] = b"61"; -/// Hold header `String` for [61](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code +/// Hold header `String` for [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct NotAuthorized(String); diff --git a/src/client/connection/response/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs index 35ad475..933b694 100644 --- a/src/client/connection/response/certificate/not_valid.rs +++ b/src/client/connection/response/certificate/not_valid.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"62"; +/// [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code +pub const CODE: &[u8] = b"62"; -/// Hold header `String` for [62](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code +/// Hold header `String` for [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct NotValid(String); diff --git a/src/client/connection/response/certificate/required.rs b/src/client/connection/response/certificate/required.rs index df0ef63..3d8f48d 100644 --- a/src/client/connection/response/certificate/required.rs +++ b/src/client/connection/response/certificate/required.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"60"; +/// [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code +pub const CODE: &[u8] = b"60"; -/// Hold header `String` for [60](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code +/// Hold header `String` for [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct Required(String); diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs index 8d24b62..93143f7 100644 --- a/src/client/connection/response/input/default.rs +++ b/src/client/connection/response/input/default.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"10"; +/// [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +pub const CODE: &[u8] = b"10"; -/// Hold header `String` for [10](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +/// Hold header `String` for [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct Default(String); diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs index cc6ab5d..9219d39 100644 --- a/src/client/connection/response/input/sensitive.rs +++ b/src/client/connection/response/input/sensitive.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"11"; +/// [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +pub const CODE: &[u8] = b"11"; -/// Hold header `String` for [11](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +/// Hold header `String` for [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct Sensitive(String); diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 6180f2e..a905318 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -4,8 +4,12 @@ pub mod header; pub use error::Error; pub use header::Header; -const CODE: &[u8] = b"20"; +/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code +pub const CODE: &[u8] = b"20"; +/// Holder for [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code +/// * this response type MAY contain body data +/// * the header has closed members to require valid construction pub struct Default { pub header: Header, pub content: Option>, From 5229cdae858ebe210c28ab3b1eb19f46da62a90e Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 02:18:02 +0200 Subject: [PATCH 366/392] reorganize redirection structs format: make constructors lazy, parse members on get --- src/client/connection/response/redirect.rs | 346 ++++++++---------- .../connection/response/redirect/error.rs | 59 ++- .../connection/response/redirect/permanent.rs | 79 ++++ .../response/redirect/permanent/error.rs | 32 ++ .../connection/response/redirect/temporary.rs | 79 ++++ .../response/redirect/temporary/error.rs | 32 ++ 6 files changed, 414 insertions(+), 213 deletions(-) create mode 100644 src/client/connection/response/redirect/permanent.rs create mode 100644 src/client/connection/response/redirect/permanent/error.rs create mode 100644 src/client/connection/response/redirect/temporary.rs create mode 100644 src/client/connection/response/redirect/temporary/error.rs diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index 2554fdf..48bd610 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -1,16 +1,23 @@ pub mod error; -pub use error::Error; +pub mod permanent; +pub mod temporary; -use glib::{GStringPtr, Uri, UriFlags}; +pub use error::{Error, UriError}; +pub use permanent::Permanent; +pub use temporary::Temporary; -const TEMPORARY: (u8, &str) = (30, "Temporary redirect"); -const PERMANENT: (u8, &str) = (31, "Permanent redirect"); +// Local dependencies +use glib::{Uri, UriFlags}; + +const CODE: u8 = b'3'; + +/// [Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) statuses pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - Temporary { target: String }, + Temporary(Temporary), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Permanent { target: String }, + Permanent(Permanent), } impl Redirect { @@ -18,210 +25,161 @@ impl Redirect { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { - 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, - } - .0 - } - - /// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection), - /// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base` - /// * fragment implementation uncompleted @TODO - pub fn to_uri(&self, base: &Uri) -> Result { - match Uri::build( - UriFlags::NONE, - base.scheme().as_str(), - None, // unexpected - base.host().as_deref(), - base.port(), - base.path().as_str(), - // > If a server sends a redirection in response to a request with a query string, - // > the client MUST NOT apply the query string to the new location - None, - // > A server SHOULD NOT include fragments in redirections, - // > but if one is given, and a client already has a fragment it could apply (from the original URI), - // > it is up to the client which fragment to apply. - None, // @TODO - ) - .parse_relative( - &{ - // URI started with double slash yet not supported by Glib function - // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 - let t = self.target(); - match t.strip_prefix("//") { - Some(p) => { - let postfix = p.trim_start_matches(":"); - format!( - "{}://{}", - base.scheme(), - if postfix.is_empty() { - match base.host() { - Some(h) => format!("{h}/"), - None => return Err(Error::BaseHost), - } - } else { - postfix.to_string() - } - ) - } - None => t.to_string(), - } + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Temporary( + Temporary::from_utf8(buffer).map_err(Error::Temporary)?, + )), + b'1' => Ok(Self::Permanent( + Permanent::from_utf8(buffer).map_err(Error::Permanent)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), }, - UriFlags::NONE, - ) { - Ok(absolute) => Ok(absolute), - Err(e) => Err(Error::Uri(e)), + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn target(&self) -> &str { + pub fn target(&self) -> Result<&str, Error> { match self { - Self::Permanent { target } => target, - Self::Temporary { target } => target, + Self::Temporary(temporary) => temporary.target().map_err(Error::Temporary), + Self::Permanent(permanent) => permanent.target().map_err(Error::Permanent), + } + } + + pub fn as_str(&self) -> &str { + match self { + Self::Temporary(temporary) => temporary.as_str(), + Self::Permanent(permanent) => permanent.as_str(), + } + } + + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Temporary(temporary) => temporary.as_bytes(), + Self::Permanent(permanent) => permanent.as_bytes(), + } + } + + pub fn uri(&self, base: &Uri) -> Result { + match self { + Self::Temporary(temporary) => temporary.uri(base).map_err(Error::Temporary), + Self::Permanent(permanent) => permanent.uri(base).map_err(Error::Permanent), } } } -impl std::fmt::Display for Redirect { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Permanent { .. } => PERMANENT, - Self::Temporary { .. } => TEMPORARY, +// Tools + +/// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection), +/// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base` +/// * fragment implementation uncompleted @TODO +fn uri(target: &str, base: &Uri) -> Result { + match Uri::build( + UriFlags::NONE, + base.scheme().as_str(), + None, // unexpected + base.host().as_deref(), + base.port(), + base.path().as_str(), + // > If a server sends a redirection in response to a request with a query string, + // > the client MUST NOT apply the query string to the new location + None, + // > A server SHOULD NOT include fragments in redirections, + // > but if one is given, and a client already has a fragment it could apply (from the original URI), + // > it is up to the client which fragment to apply. + None, // @TODO + ) + .parse_relative( + &{ + // URI started with double slash yet not supported by Glib function + // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + let t = target; + match t.strip_prefix("//") { + Some(p) => { + let postfix = p.trim_start_matches(":"); + format!( + "{}://{}", + base.scheme(), + if postfix.is_empty() { + match base.host() { + Some(h) => format!("{h}/"), + None => return Err(UriError::BaseHost), + } + } else { + postfix.to_string() + } + ) + } + None => t.to_string(), } - .1 - ) - } -} - -impl std::str::FromStr for Redirect { - type Err = Error; - fn from_str(header: &str) -> Result { - 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 { - match value { - Some(target) => { - let target = target.trim(); - if target.is_empty() { - Err(Error::Target) - } else { - Ok(target.to_string()) - } - } - None => Err(Error::Target), + }, + UriFlags::NONE, + ) { + Ok(absolute) => Ok(absolute), + Err(e) => Err(UriError::ParseRelative(e)), } } #[test] fn test() { - 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.0); - assert_eq!(temporary.to_string(), TEMPORARY.1); - - let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); - assert_eq!(permanent.target(), "/uri"); - assert_eq!(permanent.to_code(), PERMANENT.0); - assert_eq!(permanent.to_string(), PERMANENT.1); - } - { - let base = Uri::build( - UriFlags::NONE, - "gemini", - None, - Some("geminiprotocol.net"), - -1, - "/path/", - Some("query"), - Some("fragment"), - ); - assert_eq!( - Redirect::from_str("30 /uri\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/uri" - ); - assert_eq!( - Redirect::from_str("30 uri\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/path/uri" - ); - assert_eq!( - Redirect::from_str("30 gemini://test.host/uri\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://test.host/uri" - ); - assert_eq!( - Redirect::from_str("30 //\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/" - ); - assert_eq!( - Redirect::from_str("30 //geminiprotocol.net/path\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/path" - ); - assert_eq!( - Redirect::from_str("30 //:\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/" - ); + /// Test common assertion rules + fn t(base: &Uri, source: &str, target: &str) { + let b = source.as_bytes(); + let r = Redirect::from_utf8(b).unwrap(); + assert!(r.uri(base).is_ok_and(|u| u.to_string() == target)); + assert_eq!(r.as_str(), source); + assert_eq!(r.as_bytes(), b); } + // common base + let base = Uri::build( + UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); + // codes test + t( + &base, + "30 gemini://geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", + ); + t( + &base, + "31 gemini://geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", + ); + // relative test + t( + &base, + "31 path\r\n", + "gemini://geminiprotocol.net/path/path", + ); + t( + &base, + "31 //geminiprotocol.net\r\n", + "gemini://geminiprotocol.net", + ); + t( + &base, + "31 //geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", + ); + t(&base, "31 /path\r\n", "gemini://geminiprotocol.net/path"); + t(&base, "31 //:\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 //\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 /\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 ../\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 ..\r\n", "gemini://geminiprotocol.net/"); } diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index eeaf2ed..38aaab1 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -1,34 +1,55 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - BaseHost, - Uri(glib::Error), - Protocol, - Target, - Utf8Error(Utf8Error), + FirstByte(u8), + Permanent(super::permanent::Error), + SecondByte(u8), + Temporary(super::temporary::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::BaseHost => { - write!(f, "Base host required") + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } - Self::Uri(e) => { - write!(f, "URI error: {e}") + Self::Permanent(e) => { + write!(f, "Permanent parse error: {e}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") } - Self::Protocol => { - write!(f, "Protocol error") + Self::Temporary(e) => { + write!(f, "Temporary parse error: {e}") } - Self::Target => { - write!(f, "Target error") + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") + } + } + } +} + +/// Handle `super::uri` method +#[derive(Debug)] +pub enum UriError { + BaseHost, + ParseRelative(glib::Error), +} + +impl Display for UriError { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::BaseHost => { + write!(f, "URI base host required") + } + Self::ParseRelative(e) => { + write!(f, "URI parse relative error: {e}") } } } diff --git a/src/client/connection/response/redirect/permanent.rs b/src/client/connection/response/redirect/permanent.rs new file mode 100644 index 0000000..843e929 --- /dev/null +++ b/src/client/connection/response/redirect/permanent.rs @@ -0,0 +1,79 @@ +pub mod error; +pub use error::Error; + +// Local dependencies + +use glib::Uri; + +/// [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code +pub const CODE: &[u8] = b"31"; + +/// Hold header `String` for [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Permanent(String); + +impl Permanent { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get raw target for `Self` + /// * return `Err` if the required target is empty + pub fn target(&self) -> Result<&str, Error> { + self.0 + .get(2..) + .map(|s| s.trim()) + .filter(|x| !x.is_empty()) + .ok_or(Error::TargetEmpty) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub fn uri(&self, base: &Uri) -> Result { + super::uri(self.target()?, base).map_err(Error::Uri) + } +} + +#[test] +fn test() { + const BUFFER: &str = "31 gemini://geminiprotocol.net/path\r\n"; + let base = Uri::build( + glib::UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); + let permanent = Permanent::from_utf8(BUFFER.as_bytes()).unwrap(); + assert!(permanent.target().is_ok()); + assert!( + permanent + .uri(&base) + .is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path") + ); + assert!(Permanent::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/redirect/permanent/error.rs b/src/client/connection/response/redirect/permanent/error.rs new file mode 100644 index 0000000..0d0e81d --- /dev/null +++ b/src/client/connection/response/redirect/permanent/error.rs @@ -0,0 +1,32 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + TargetEmpty, + Uri(super::super::UriError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::TargetEmpty => { + write!(f, "Expected target is empty") + } + Self::Uri(e) => { + write!(f, "URI parse error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/redirect/temporary.rs b/src/client/connection/response/redirect/temporary.rs new file mode 100644 index 0000000..df41731 --- /dev/null +++ b/src/client/connection/response/redirect/temporary.rs @@ -0,0 +1,79 @@ +pub mod error; +pub use error::Error; + +// Local dependencies + +use glib::Uri; + +/// [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code +pub const CODE: &[u8] = b"30"; + +/// Hold header `String` for [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Temporary(String); + +impl Temporary { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get raw target for `Self` + /// * return `Err` if the required target is empty + pub fn target(&self) -> Result<&str, Error> { + self.0 + .get(2..) + .map(|s| s.trim()) + .filter(|x| !x.is_empty()) + .ok_or(Error::TargetEmpty) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub fn uri(&self, base: &Uri) -> Result { + super::uri(self.target()?, base).map_err(Error::Uri) + } +} + +#[test] +fn test() { + const BUFFER: &str = "30 gemini://geminiprotocol.net/path\r\n"; + let base = Uri::build( + glib::UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); + let temporary = Temporary::from_utf8(BUFFER.as_bytes()).unwrap(); + assert!(temporary.target().is_ok()); + assert!( + temporary + .uri(&base) + .is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path") + ); + assert!(Temporary::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err()) +} diff --git a/src/client/connection/response/redirect/temporary/error.rs b/src/client/connection/response/redirect/temporary/error.rs new file mode 100644 index 0000000..0d0e81d --- /dev/null +++ b/src/client/connection/response/redirect/temporary/error.rs @@ -0,0 +1,32 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + TargetEmpty, + Uri(super::super::UriError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::TargetEmpty => { + write!(f, "Expected target is empty") + } + Self::Uri(e) => { + write!(f, "URI parse error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From f513747e862ae318532f942f8860e1e85f0a3485 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 02:21:03 +0200 Subject: [PATCH 367/392] add alias getters test --- src/client/connection/response/redirect/permanent.rs | 5 ++++- src/client/connection/response/redirect/temporary.rs | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/connection/response/redirect/permanent.rs b/src/client/connection/response/redirect/permanent.rs index 843e929..e8e6371 100644 --- a/src/client/connection/response/redirect/permanent.rs +++ b/src/client/connection/response/redirect/permanent.rs @@ -58,6 +58,7 @@ impl Permanent { #[test] fn test() { const BUFFER: &str = "31 gemini://geminiprotocol.net/path\r\n"; + let bytes = BUFFER.as_bytes(); let base = Uri::build( glib::UriFlags::NONE, "gemini", @@ -68,7 +69,9 @@ fn test() { Some("query"), Some("fragment"), ); - let permanent = Permanent::from_utf8(BUFFER.as_bytes()).unwrap(); + let permanent = Permanent::from_utf8(bytes).unwrap(); + assert_eq!(permanent.as_str(), BUFFER); + assert_eq!(permanent.as_bytes(), bytes); assert!(permanent.target().is_ok()); assert!( permanent diff --git a/src/client/connection/response/redirect/temporary.rs b/src/client/connection/response/redirect/temporary.rs index df41731..a131336 100644 --- a/src/client/connection/response/redirect/temporary.rs +++ b/src/client/connection/response/redirect/temporary.rs @@ -58,6 +58,7 @@ impl Temporary { #[test] fn test() { const BUFFER: &str = "30 gemini://geminiprotocol.net/path\r\n"; + let bytes = BUFFER.as_bytes(); let base = Uri::build( glib::UriFlags::NONE, "gemini", @@ -69,6 +70,8 @@ fn test() { Some("fragment"), ); let temporary = Temporary::from_utf8(BUFFER.as_bytes()).unwrap(); + assert_eq!(temporary.as_str(), BUFFER); + assert_eq!(temporary.as_bytes(), bytes); assert!(temporary.target().is_ok()); assert!( temporary From 3b24625d66f45c3fb763759424f5482f869ef308 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 03:01:19 +0200 Subject: [PATCH 368/392] implement `message_or_default` method, add comments --- src/client/connection/response/certificate.rs | 14 ++++++++++++++ .../response/certificate/not_authorized.rs | 15 ++++++++++++++- .../connection/response/certificate/not_valid.rs | 15 ++++++++++++++- .../connection/response/certificate/required.rs | 15 ++++++++++++++- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index e67ded4..07e2891 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -51,6 +51,8 @@ impl Certificate { // Getters + /// Get optional message for `Self` + /// * return `None` if the message is empty (not provided by server) pub fn message(&self) -> Option<&str> { match self { Self::Required(required) => required.message(), @@ -59,6 +61,17 @@ impl Certificate { } } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Required(required) => required.message_or_default(), + Self::NotAuthorized(not_authorized) => not_authorized.message_or_default(), + Self::NotValid(not_valid) => not_valid.message_or_default(), + } + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { match self { Self::Required(required) => required.as_str(), @@ -67,6 +80,7 @@ impl Certificate { } } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { match self { Self::Required(required) => required.as_bytes(), diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs index 980d9ac..0c73ae7 100644 --- a/src/client/connection/response/certificate/not_authorized.rs +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code pub const CODE: &[u8] = b"61"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Certificate is not authorized"; + /// Hold header `String` for [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -29,15 +34,23 @@ impl NotAuthorized { // Getters /// Get optional message for `Self` - /// * return `None` if the message is empty + /// * return `None` if the message is empty (not provided by server) pub fn message(&self) -> Option<&str> { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } diff --git a/src/client/connection/response/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs index 933b694..58198a8 100644 --- a/src/client/connection/response/certificate/not_valid.rs +++ b/src/client/connection/response/certificate/not_valid.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code pub const CODE: &[u8] = b"62"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Certificate is not valid"; + /// Hold header `String` for [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -29,15 +34,23 @@ impl NotValid { // Getters /// Get optional message for `Self` - /// * return `None` if the message is empty + /// * return `None` if the message is empty (not provided by server) pub fn message(&self) -> Option<&str> { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } diff --git a/src/client/connection/response/certificate/required.rs b/src/client/connection/response/certificate/required.rs index 3d8f48d..105185a 100644 --- a/src/client/connection/response/certificate/required.rs +++ b/src/client/connection/response/certificate/required.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code pub const CODE: &[u8] = b"60"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Certificate required"; + /// Hold header `String` for [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -29,15 +34,23 @@ impl Required { // Getters /// Get optional message for `Self` - /// * return `None` if the message is empty + /// * return `None` if the message is empty (not provided by server) pub fn message(&self) -> Option<&str> { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } From d565d56c1728cec3794b570774f986b2bb2eeb77 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 03:20:40 +0200 Subject: [PATCH 369/392] implement `message_or_default` method, add comments, add missed members test --- src/client/connection/response/input.rs | 11 +++++++++++ .../connection/response/input/default.rs | 18 +++++++++++++++++- .../connection/response/input/sensitive.rs | 18 +++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 549b13e..0cd9857 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -48,6 +48,16 @@ impl Input { } } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Default(default) => default.message_or_default(), + Self::Sensitive(sensitive) => sensitive.message_or_default(), + } + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { match self { Self::Default(default) => default.as_str(), @@ -55,6 +65,7 @@ impl Input { } } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { match self { Self::Default(default) => default.as_bytes(), diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs index 93143f7..9b9f096 100644 --- a/src/client/connection/response/input/default.rs +++ b/src/client/connection/response/input/default.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code pub const CODE: &[u8] = b"10"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Input expected"; + /// Hold header `String` for [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -34,10 +39,18 @@ impl Default { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } @@ -48,14 +61,17 @@ fn test() { // ok let default = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); assert_eq!(default.message(), Some("Default")); + assert_eq!(default.message_or_default(), "Default"); assert_eq!(default.as_str(), "10 Default\r\n"); + assert_eq!(default.as_bytes(), "10 Default\r\n".as_bytes()); let default = Default::from_utf8("10\r\n".as_bytes()).unwrap(); assert_eq!(default.message(), None); + assert_eq!(default.message_or_default(), DEFAULT_MESSAGE); assert_eq!(default.as_str(), "10\r\n"); + assert_eq!(default.as_bytes(), "10\r\n".as_bytes()); // err - // @TODO assert!(Default::from_utf8("10Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("Fail".as_bytes()).is_err()); diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs index 9219d39..4646776 100644 --- a/src/client/connection/response/input/sensitive.rs +++ b/src/client/connection/response/input/sensitive.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code pub const CODE: &[u8] = b"11"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Sensitive input expected"; + /// Hold header `String` for [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -34,10 +39,18 @@ impl Sensitive { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } @@ -48,14 +61,17 @@ fn test() { // ok let sensitive = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); assert_eq!(sensitive.message(), Some("Sensitive")); + assert_eq!(sensitive.message_or_default(), "Sensitive"); assert_eq!(sensitive.as_str(), "11 Sensitive\r\n"); + assert_eq!(sensitive.as_bytes(), "11 Sensitive\r\n".as_bytes()); let sensitive = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); assert_eq!(sensitive.message(), None); + assert_eq!(sensitive.message_or_default(), DEFAULT_MESSAGE); assert_eq!(sensitive.as_str(), "11\r\n"); + assert_eq!(sensitive.as_bytes(), "11\r\n".as_bytes()); // err - // @TODO assert!(Sensitive::from_utf8("11Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err()); From 064c4107f33174dfe872f861273415c8bd3d4fb9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 03:21:59 +0200 Subject: [PATCH 370/392] reduce local var names len --- .../connection/response/input/default.rs | 20 +++++++++---------- .../connection/response/input/sensitive.rs | 20 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs index 9b9f096..4a5a3df 100644 --- a/src/client/connection/response/input/default.rs +++ b/src/client/connection/response/input/default.rs @@ -59,17 +59,17 @@ impl Default { #[test] fn test() { // ok - let default = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); - assert_eq!(default.message(), Some("Default")); - assert_eq!(default.message_or_default(), "Default"); - assert_eq!(default.as_str(), "10 Default\r\n"); - assert_eq!(default.as_bytes(), "10 Default\r\n".as_bytes()); + let d = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Default")); + assert_eq!(d.message_or_default(), "Default"); + assert_eq!(d.as_str(), "10 Default\r\n"); + assert_eq!(d.as_bytes(), "10 Default\r\n".as_bytes()); - let default = Default::from_utf8("10\r\n".as_bytes()).unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.message_or_default(), DEFAULT_MESSAGE); - assert_eq!(default.as_str(), "10\r\n"); - assert_eq!(default.as_bytes(), "10\r\n".as_bytes()); + let d = Default::from_utf8("10\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "10\r\n"); + assert_eq!(d.as_bytes(), "10\r\n".as_bytes()); // err assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs index 4646776..594c8fb 100644 --- a/src/client/connection/response/input/sensitive.rs +++ b/src/client/connection/response/input/sensitive.rs @@ -59,17 +59,17 @@ impl Sensitive { #[test] fn test() { // ok - let sensitive = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); - assert_eq!(sensitive.message(), Some("Sensitive")); - assert_eq!(sensitive.message_or_default(), "Sensitive"); - assert_eq!(sensitive.as_str(), "11 Sensitive\r\n"); - assert_eq!(sensitive.as_bytes(), "11 Sensitive\r\n".as_bytes()); + let s = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); + assert_eq!(s.message(), Some("Sensitive")); + assert_eq!(s.message_or_default(), "Sensitive"); + assert_eq!(s.as_str(), "11 Sensitive\r\n"); + assert_eq!(s.as_bytes(), "11 Sensitive\r\n".as_bytes()); - let sensitive = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); - assert_eq!(sensitive.message(), None); - assert_eq!(sensitive.message_or_default(), DEFAULT_MESSAGE); - assert_eq!(sensitive.as_str(), "11\r\n"); - assert_eq!(sensitive.as_bytes(), "11\r\n".as_bytes()); + let s = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); + assert_eq!(s.message(), None); + assert_eq!(s.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(s.as_str(), "11\r\n"); + assert_eq!(s.as_bytes(), "11\r\n".as_bytes()); // err assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err()); From 0c75da793ff9d4dde0a343ee680e79b29bf77a72 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 03:26:39 +0200 Subject: [PATCH 371/392] add missed tests members, enshort local var names --- .../response/certificate/not_authorized.rs | 16 ++++++++++------ .../connection/response/certificate/not_valid.rs | 16 ++++++++++------ .../connection/response/certificate/required.rs | 16 ++++++++++------ 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs index 0c73ae7..b10d6ff 100644 --- a/src/client/connection/response/certificate/not_authorized.rs +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -59,13 +59,17 @@ impl NotAuthorized { #[test] fn test() { // ok - let not_authorized = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap(); - assert_eq!(not_authorized.message(), Some("Not Authorized")); - assert_eq!(not_authorized.as_str(), "61 Not Authorized\r\n"); + let na = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap(); + assert_eq!(na.message(), Some("Not Authorized")); + assert_eq!(na.message_or_default(), "Not Authorized"); + assert_eq!(na.as_str(), "61 Not Authorized\r\n"); + assert_eq!(na.as_bytes(), "61 Not Authorized\r\n".as_bytes()); - let not_authorized = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap(); - assert_eq!(not_authorized.message(), None); - assert_eq!(not_authorized.as_str(), "61\r\n"); + let na = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap(); + assert_eq!(na.message(), None); + assert_eq!(na.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(na.as_str(), "61\r\n"); + assert_eq!(na.as_bytes(), "61\r\n".as_bytes()); // err assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err()); diff --git a/src/client/connection/response/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs index 58198a8..94e847c 100644 --- a/src/client/connection/response/certificate/not_valid.rs +++ b/src/client/connection/response/certificate/not_valid.rs @@ -59,13 +59,17 @@ impl NotValid { #[test] fn test() { // ok - let not_valid = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap(); - assert_eq!(not_valid.message(), Some("Not Valid")); - assert_eq!(not_valid.as_str(), "62 Not Valid\r\n"); + let nv = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap(); + assert_eq!(nv.message(), Some("Not Valid")); + assert_eq!(nv.message_or_default(), "Not Valid"); + assert_eq!(nv.as_str(), "62 Not Valid\r\n"); + assert_eq!(nv.as_bytes(), "62 Not Valid\r\n".as_bytes()); - let not_valid = NotValid::from_utf8("62\r\n".as_bytes()).unwrap(); - assert_eq!(not_valid.message(), None); - assert_eq!(not_valid.as_str(), "62\r\n"); + let nv = NotValid::from_utf8("62\r\n".as_bytes()).unwrap(); + assert_eq!(nv.message(), None); + assert_eq!(nv.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(nv.as_str(), "62\r\n"); + assert_eq!(nv.as_bytes(), "62\r\n".as_bytes()); // err // @TODO assert!(NotValid::from_utf8("62Fail\r\n".as_bytes()).is_err()); diff --git a/src/client/connection/response/certificate/required.rs b/src/client/connection/response/certificate/required.rs index 105185a..b44585c 100644 --- a/src/client/connection/response/certificate/required.rs +++ b/src/client/connection/response/certificate/required.rs @@ -59,13 +59,17 @@ impl Required { #[test] fn test() { // ok - let required = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap(); - assert_eq!(required.message(), Some("Required")); - assert_eq!(required.as_str(), "60 Required\r\n"); + let r = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap(); + assert_eq!(r.message(), Some("Required")); + assert_eq!(r.message_or_default(), "Required"); + assert_eq!(r.as_str(), "60 Required\r\n"); + assert_eq!(r.as_bytes(), "60 Required\r\n".as_bytes()); - let required = Required::from_utf8("60\r\n".as_bytes()).unwrap(); - assert_eq!(required.message(), None); - assert_eq!(required.as_str(), "60\r\n"); + let r = Required::from_utf8("60\r\n".as_bytes()).unwrap(); + assert_eq!(r.message(), None); + assert_eq!(r.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(r.as_str(), "60\r\n"); + assert_eq!(r.as_bytes(), "60\r\n".as_bytes()); // err assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err()); From e96ff688b364d3bcf276c1edeb2760677b2555bc Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 04:43:56 +0200 Subject: [PATCH 372/392] update permanent status codes api --- src/client/connection/response/failure.rs | 11 +- .../connection/response/failure/error.rs | 6 +- .../connection/response/failure/permanent.rs | 230 +++++++----------- .../response/failure/permanent/bad_request.rs | 78 ++++++ .../failure/permanent/bad_request/error.rs | 24 ++ .../response/failure/permanent/default.rs | 78 ++++++ .../failure/permanent/default/error.rs | 24 ++ .../response/failure/permanent/error.rs | 45 +++- .../response/failure/permanent/gone.rs | 78 ++++++ .../response/failure/permanent/gone/error.rs | 24 ++ .../response/failure/permanent/not_found.rs | 78 ++++++ .../failure/permanent/not_found/error.rs | 24 ++ .../permanent/proxy_request_refused.rs | 78 ++++++ .../permanent/proxy_request_refused/error.rs | 24 ++ 14 files changed, 644 insertions(+), 158 deletions(-) create mode 100644 src/client/connection/response/failure/permanent/bad_request.rs create mode 100644 src/client/connection/response/failure/permanent/bad_request/error.rs create mode 100644 src/client/connection/response/failure/permanent/default.rs create mode 100644 src/client/connection/response/failure/permanent/default/error.rs create mode 100644 src/client/connection/response/failure/permanent/gone.rs create mode 100644 src/client/connection/response/failure/permanent/gone/error.rs create mode 100644 src/client/connection/response/failure/permanent/not_found.rs create mode 100644 src/client/connection/response/failure/permanent/not_found/error.rs create mode 100644 src/client/connection/response/failure/permanent/proxy_request_refused.rs create mode 100644 src/client/connection/response/failure/permanent/proxy_request_refused/error.rs diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 40c8abf..419af23 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -21,7 +21,7 @@ impl Failure { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.first() { - Some(byte) => match byte { + Some(b) => match b { b'4' => match Temporary::from_utf8(buffer) { Ok(input) => Ok(Self::Temporary(input)), Err(e) => Err(Error::Temporary(e)), @@ -30,7 +30,7 @@ impl Failure { Ok(failure) => Ok(Self::Permanent(failure)), Err(e) => Err(Error::Permanent(e)), }, - _ => Err(Error::Code), + b => Err(Error::Code(*b)), }, None => Err(Error::Protocol), } @@ -38,13 +38,6 @@ impl Failure { // 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(), diff --git a/src/client/connection/response/failure/error.rs b/src/client/connection/response/failure/error.rs index 7725b92..056f714 100644 --- a/src/client/connection/response/failure/error.rs +++ b/src/client/connection/response/failure/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, + Code(u8), Permanent(super::permanent::Error), Protocol, Temporary(super::temporary::Error), @@ -11,8 +11,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Code group error") + Self::Code(b) => { + write!(f, "Unexpected status code byte: {b}") } Self::Permanent(e) => { write!(f, "Permanent failure group error: {e}") diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e2ab9e0..ffe4ea3 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -1,24 +1,31 @@ +pub mod bad_request; +pub mod default; pub mod error; -pub use error::Error; +pub mod gone; +pub mod not_found; +pub mod proxy_request_refused; -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"); +pub use bad_request::BadRequest; +pub use default::Default; +pub use error::Error; +pub use gone::Gone; +pub use not_found::NotFound; +pub use proxy_request_refused::ProxyRequestRefused; + +const CODE: u8 = b'5'; /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure pub enum Permanent { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50 - Default { message: Option }, + Default(Default), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found - NotFound { message: Option }, + NotFound(NotFound), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone - Gone { message: Option }, + Gone(Gone), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused - ProxyRequestRefused { message: Option }, + ProxyRequestRefused(ProxyRequestRefused), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request - BadRequest { message: Option }, + BadRequest(BadRequest), } impl Permanent { @@ -26,154 +33,105 @@ impl Permanent { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::NotFound( + NotFound::from_utf8(buffer).map_err(Error::NotFound)?, + )), + b'2' => Ok(Self::Gone(Gone::from_utf8(buffer).map_err(Error::Gone)?)), + b'3' => Ok(Self::ProxyRequestRefused( + ProxyRequestRefused::from_utf8(buffer) + .map_err(Error::ProxyRequestRefused)?, + )), + b'9' => Ok(Self::BadRequest( + BadRequest::from_utf8(buffer).map_err(Error::BadRequest)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // 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, + Self::Default(default) => default.message(), + Self::NotFound(not_found) => not_found.message(), + Self::Gone(gone) => gone.message(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.message(), + Self::BadRequest(bad_request) => bad_request.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 { .. } => DEFAULT, - Self::NotFound { .. } => NOT_FOUND, - Self::Gone { .. } => GONE, - Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED, - Self::BadRequest { .. } => BAD_REQUEST, + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Default(default) => default.message_or_default(), + Self::NotFound(not_found) => not_found.message_or_default(), + Self::Gone(gone) => gone.message_or_default(), + Self::ProxyRequestRefused(proxy_request_refused) => { + proxy_request_refused.message_or_default() } - .1 - ) + Self::BadRequest(bad_request) => bad_request.message_or_default(), + } } -} -impl std::str::FromStr for Permanent { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("50") { - return Ok(Self::Default { - message: message(postfix), - }); + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::NotFound(not_found) => not_found.as_str(), + Self::Gone(gone) => gone.as_str(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_str(), + Self::BadRequest(bad_request) => bad_request.as_str(), } - 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 { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::NotFound(not_found) => not_found.as_bytes(), + Self::Gone(gone) => gone.as_bytes(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_bytes(), + Self::BadRequest(bad_request) => bad_request.as_bytes(), + } } } #[test] -fn test_from_str() { - use std::str::FromStr; - +fn test() { + fn t(source: &str, message: Option<&str>) { + let b = source.as_bytes(); + let i = Permanent::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } // 50 - let default = Permanent::from_str("50 Message\r\n").unwrap(); - assert_eq!(default.message(), Some("Message")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Permanent::from_str("50\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - + t("50 Message\r\n", Some("Message")); + t("50\r\n", None); // 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); - assert_eq!(not_found.to_string(), NOT_FOUND.1); - - 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); - assert_eq!(not_found.to_string(), NOT_FOUND.1); - + t("51 Message\r\n", Some("Message")); + t("51\r\n", None); // 52 - let gone = Permanent::from_str("52 Message\r\n").unwrap(); - assert_eq!(gone.message(), Some("Message")); - assert_eq!(gone.to_code(), GONE.0); - assert_eq!(gone.to_string(), GONE.1); - - let gone = Permanent::from_str("52\r\n").unwrap(); - assert_eq!(gone.message(), None); - assert_eq!(gone.to_code(), GONE.0); - assert_eq!(gone.to_string(), GONE.1); - + t("52 Message\r\n", Some("Message")); + t("52\r\n", None); // 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); - assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); - - 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); - assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); - + t("53 Message\r\n", Some("Message")); + t("53\r\n", None); // 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); - assert_eq!(bad_request.to_string(), BAD_REQUEST.1); - - 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); - assert_eq!(bad_request.to_string(), BAD_REQUEST.1); + t("59 Message\r\n", Some("Message")); + t("59\r\n", None); } diff --git a/src/client/connection/response/failure/permanent/bad_request.rs b/src/client/connection/response/failure/permanent/bad_request.rs new file mode 100644 index 0000000..8cfa6f9 --- /dev/null +++ b/src/client/connection/response/failure/permanent/bad_request.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code +pub const CODE: &[u8] = b"59"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Bad request"; + +/// Hold header `String` for [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct BadRequest(String); + +impl BadRequest { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let br = BadRequest::from_utf8("59 Message\r\n".as_bytes()).unwrap(); + assert_eq!(br.message(), Some("Message")); + assert_eq!(br.message_or_default(), "Message"); + assert_eq!(br.as_str(), "59 Message\r\n"); + assert_eq!(br.as_bytes(), "59 Message\r\n".as_bytes()); + + let br = BadRequest::from_utf8("59\r\n".as_bytes()).unwrap(); + assert_eq!(br.message(), None); + assert_eq!(br.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(br.as_str(), "59\r\n"); + assert_eq!(br.as_bytes(), "59\r\n".as_bytes()); + + // err + assert!(BadRequest::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(BadRequest::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(BadRequest::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/bad_request/error.rs b/src/client/connection/response/failure/permanent/bad_request/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/bad_request/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent/default.rs b/src/client/connection/response/failure/permanent/default.rs new file mode 100644 index 0000000..466333d --- /dev/null +++ b/src/client/connection/response/failure/permanent/default.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code +pub const CODE: &[u8] = b"50"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Permanent error"; + +/// Hold header `String` for [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let d = Default::from_utf8("50 Message\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Message")); + assert_eq!(d.message_or_default(), "Message"); + assert_eq!(d.as_str(), "50 Message\r\n"); + assert_eq!(d.as_bytes(), "50 Message\r\n".as_bytes()); + + let d = Default::from_utf8("50\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "50\r\n"); + assert_eq!(d.as_bytes(), "50\r\n".as_bytes()); + + // err + assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/default/error.rs b/src/client/connection/response/failure/permanent/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent/error.rs b/src/client/connection/response/failure/permanent/error.rs index 5cf1cf6..df334f5 100644 --- a/src/client/connection/response/failure/permanent/error.rs +++ b/src/client/connection/response/failure/permanent/error.rs @@ -1,22 +1,47 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + BadRequest(super::bad_request::Error), + Default(super::default::Error), + FirstByte(u8), + Gone(super::gone::Error), + NotFound(super::not_found::Error), + ProxyRequestRefused(super::proxy_request_refused::Error), + SecondByte(u8), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::BadRequest(e) => { + write!(f, "BadRequest parse error: {e}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") + } + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") + } + Self::Gone(e) => { + write!(f, "Gone parse error: {e}") + } + Self::NotFound(e) => { + write!(f, "NotFound parse error: {e}") + } + Self::ProxyRequestRefused(e) => { + write!(f, "ProxyRequestRefused parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/failure/permanent/gone.rs b/src/client/connection/response/failure/permanent/gone.rs new file mode 100644 index 0000000..f93d068 --- /dev/null +++ b/src/client/connection/response/failure/permanent/gone.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code +pub const CODE: &[u8] = b"52"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Resource gone"; + +/// Hold header `String` for [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Gone(String); + +impl Gone { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let g = Gone::from_utf8("52 Message\r\n".as_bytes()).unwrap(); + assert_eq!(g.message(), Some("Message")); + assert_eq!(g.message_or_default(), "Message"); + assert_eq!(g.as_str(), "52 Message\r\n"); + assert_eq!(g.as_bytes(), "52 Message\r\n".as_bytes()); + + let g = Gone::from_utf8("52\r\n".as_bytes()).unwrap(); + assert_eq!(g.message(), None); + assert_eq!(g.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(g.as_str(), "52\r\n"); + assert_eq!(g.as_bytes(), "52\r\n".as_bytes()); + + // err + assert!(Gone::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Gone::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Gone::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/gone/error.rs b/src/client/connection/response/failure/permanent/gone/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/gone/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent/not_found.rs b/src/client/connection/response/failure/permanent/not_found.rs new file mode 100644 index 0000000..d5ddca9 --- /dev/null +++ b/src/client/connection/response/failure/permanent/not_found.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code +pub const CODE: &[u8] = b"51"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Not Found"; + +/// Hold header `String` for [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotFound(String); + +impl NotFound { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let nf = NotFound::from_utf8("51 Message\r\n".as_bytes()).unwrap(); + assert_eq!(nf.message(), Some("Message")); + assert_eq!(nf.message_or_default(), "Message"); + assert_eq!(nf.as_str(), "51 Message\r\n"); + assert_eq!(nf.as_bytes(), "51 Message\r\n".as_bytes()); + + let nf = NotFound::from_utf8("51\r\n".as_bytes()).unwrap(); + assert_eq!(nf.message(), None); + assert_eq!(nf.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(nf.as_str(), "51\r\n"); + assert_eq!(nf.as_bytes(), "51\r\n".as_bytes()); + + // err + assert!(NotFound::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(NotFound::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotFound::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/not_found/error.rs b/src/client/connection/response/failure/permanent/not_found/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/not_found/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent/proxy_request_refused.rs b/src/client/connection/response/failure/permanent/proxy_request_refused.rs new file mode 100644 index 0000000..fba229c --- /dev/null +++ b/src/client/connection/response/failure/permanent/proxy_request_refused.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code +pub const CODE: &[u8] = b"53"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Proxy request refused"; + +/// Hold header `String` for [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ProxyRequestRefused(String); + +impl ProxyRequestRefused { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let prf = ProxyRequestRefused::from_utf8("53 Message\r\n".as_bytes()).unwrap(); + assert_eq!(prf.message(), Some("Message")); + assert_eq!(prf.message_or_default(), "Message"); + assert_eq!(prf.as_str(), "53 Message\r\n"); + assert_eq!(prf.as_bytes(), "53 Message\r\n".as_bytes()); + + let prf = ProxyRequestRefused::from_utf8("53\r\n".as_bytes()).unwrap(); + assert_eq!(prf.message(), None); + assert_eq!(prf.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(prf.as_str(), "53\r\n"); + assert_eq!(prf.as_bytes(), "53\r\n".as_bytes()); + + // err + assert!(ProxyRequestRefused::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ProxyRequestRefused::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ProxyRequestRefused::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs b/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From c9a59e76eeafcdbbc58274b4b4c859bd99b9f9a8 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 05:13:40 +0200 Subject: [PATCH 373/392] optimize tests format --- .../connection/response/failure/permanent.rs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index ffe4ea3..526a208 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -112,26 +112,15 @@ impl Permanent { #[test] fn test() { - fn t(source: &str, message: Option<&str>) { + fn t(source: String, message: Option<&str>) { let b = source.as_bytes(); let i = Permanent::from_utf8(b).unwrap(); assert_eq!(i.message(), message); assert_eq!(i.as_str(), source); assert_eq!(i.as_bytes(), b); } - // 50 - t("50 Message\r\n", Some("Message")); - t("50\r\n", None); - // 51 - t("51 Message\r\n", Some("Message")); - t("51\r\n", None); - // 52 - t("52 Message\r\n", Some("Message")); - t("52\r\n", None); - // 53 - t("53 Message\r\n", Some("Message")); - t("53\r\n", None); - // 59 - t("59 Message\r\n", Some("Message")); - t("59\r\n", None); + for code in [50, 51, 52, 53, 59] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } From ea1fb8ea66687219e68dd8d22ccb0abca3143e50 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 05:13:56 +0200 Subject: [PATCH 374/392] update temporary status codes api --- src/client/connection/response/failure.rs | 42 ++++ .../connection/response/failure/temporary.rs | 231 +++++++----------- .../response/failure/temporary/cgi_error.rs | 78 ++++++ .../failure/temporary/cgi_error/error.rs | 24 ++ .../response/failure/temporary/default.rs | 78 ++++++ .../failure/temporary/default/error.rs | 24 ++ .../response/failure/temporary/error.rs | 45 +++- .../response/failure/temporary/proxy_error.rs | 78 ++++++ .../failure/temporary/proxy_error/error.rs | 24 ++ .../failure/temporary/server_unavailable.rs | 81 ++++++ .../temporary/server_unavailable/error.rs | 24 ++ .../response/failure/temporary/slow_down.rs | 81 ++++++ .../failure/temporary/slow_down/error.rs | 24 ++ 13 files changed, 682 insertions(+), 152 deletions(-) create mode 100644 src/client/connection/response/failure/temporary/cgi_error.rs create mode 100644 src/client/connection/response/failure/temporary/cgi_error/error.rs create mode 100644 src/client/connection/response/failure/temporary/default.rs create mode 100644 src/client/connection/response/failure/temporary/default/error.rs create mode 100644 src/client/connection/response/failure/temporary/proxy_error.rs create mode 100644 src/client/connection/response/failure/temporary/proxy_error/error.rs create mode 100644 src/client/connection/response/failure/temporary/server_unavailable.rs create mode 100644 src/client/connection/response/failure/temporary/server_unavailable/error.rs create mode 100644 src/client/connection/response/failure/temporary/slow_down.rs create mode 100644 src/client/connection/response/failure/temporary/slow_down/error.rs diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 419af23..1ace0ed 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -38,10 +38,52 @@ impl Failure { // Getters + /// Get optional message for `Self` + /// * return `None` if the message is empty pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), Self::Temporary(temporary) => temporary.message(), } } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.message_or_default(), + Self::Temporary(temporary) => temporary.message_or_default(), + } + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.as_str(), + Self::Temporary(temporary) => temporary.as_str(), + } + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Permanent(permanent) => permanent.as_bytes(), + Self::Temporary(temporary) => temporary.as_bytes(), + } + } +} + +#[test] +fn test() { + fn t(source: String, message: Option<&str>) { + let b = source.as_bytes(); + let i = Failure::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } + for code in [40, 41, 42, 43, 44, 50, 51, 52, 53, 59] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 768bdcd..cc20834 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -1,24 +1,31 @@ +pub mod cgi_error; +pub mod default; pub mod error; -pub use error::Error; +pub mod proxy_error; +pub mod server_unavailable; +pub mod slow_down; -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"); +pub use cgi_error::CgiError; +pub use default::Default; +pub use error::Error; +pub use proxy_error::ProxyError; +pub use server_unavailable::ServerUnavailable; +pub use slow_down::SlowDown; + +const CODE: u8 = b'4'; /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure pub enum Temporary { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40 - Default { message: Option }, + Default(Default), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable { message: Option }, + ServerUnavailable(ServerUnavailable), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError { message: Option }, + CgiError(CgiError), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError { message: Option }, + ProxyError(ProxyError), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown { message: Option }, + SlowDown(SlowDown), } impl Temporary { @@ -26,154 +33,94 @@ impl Temporary { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::ServerUnavailable( + ServerUnavailable::from_utf8(buffer) + .map_err(Error::ServerUnavailable)?, + )), + b'2' => Ok(Self::CgiError( + CgiError::from_utf8(buffer).map_err(Error::CgiError)?, + )), + b'3' => Ok(Self::ProxyError( + ProxyError::from_utf8(buffer).map_err(Error::ProxyError)?, + )), + b'4' => Ok(Self::SlowDown( + SlowDown::from_utf8(buffer).map_err(Error::SlowDown)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // 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, + Self::Default(default) => default.message(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.message(), + Self::CgiError(cgi_error) => cgi_error.message(), + Self::ProxyError(proxy_error) => proxy_error.message(), + Self::SlowDown(slow_down) => slow_down.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 { .. } => DEFAULT, - Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, - Self::CgiError { .. } => CGI_ERROR, - Self::ProxyError { .. } => PROXY_ERROR, - Self::SlowDown { .. } => SLOW_DOWN, - } - .1 - ) + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Default(default) => default.message_or_default(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.message_or_default(), + Self::CgiError(cgi_error) => cgi_error.message_or_default(), + Self::ProxyError(proxy_error) => proxy_error.message_or_default(), + Self::SlowDown(slow_down) => slow_down.message_or_default(), + } } -} -impl std::str::FromStr for Temporary { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("40") { - return Ok(Self::Default { - message: message(postfix), - }); + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.as_str(), + Self::CgiError(cgi_error) => cgi_error.as_str(), + Self::ProxyError(proxy_error) => proxy_error.as_str(), + Self::SlowDown(slow_down) => slow_down.as_str(), } - 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 { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.as_bytes(), + Self::CgiError(cgi_error) => cgi_error.as_bytes(), + Self::ProxyError(proxy_error) => proxy_error.as_bytes(), + Self::SlowDown(slow_down) => slow_down.as_bytes(), + } } } #[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); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Temporary::from_str("40\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - // 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); - assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); - - 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); - assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); - - // 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); - assert_eq!(cgi_error.to_string(), CGI_ERROR.1); - - 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); - assert_eq!(cgi_error.to_string(), CGI_ERROR.1); - - // 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); - assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); - - 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); - assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); - - // 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); - assert_eq!(slow_down.to_string(), SLOW_DOWN.1); - - 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); - assert_eq!(slow_down.to_string(), SLOW_DOWN.1); +fn test() { + fn t(source: String, message: Option<&str>) { + let b = source.as_bytes(); + let i = Temporary::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } + for code in [40, 41, 42, 43, 44] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } diff --git a/src/client/connection/response/failure/temporary/cgi_error.rs b/src/client/connection/response/failure/temporary/cgi_error.rs new file mode 100644 index 0000000..8843fa9 --- /dev/null +++ b/src/client/connection/response/failure/temporary/cgi_error.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code +pub const CODE: &[u8] = b"42"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "CGI Error"; + +/// Hold header `String` for [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct CgiError(String); + +impl CgiError { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let ce = CgiError::from_utf8("42 Message\r\n".as_bytes()).unwrap(); + assert_eq!(ce.message(), Some("Message")); + assert_eq!(ce.message_or_default(), "Message"); + assert_eq!(ce.as_str(), "42 Message\r\n"); + assert_eq!(ce.as_bytes(), "42 Message\r\n".as_bytes()); + + let ce = CgiError::from_utf8("42\r\n".as_bytes()).unwrap(); + assert_eq!(ce.message(), None); + assert_eq!(ce.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(ce.as_str(), "42\r\n"); + assert_eq!(ce.as_bytes(), "42\r\n".as_bytes()); + + // err + assert!(CgiError::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(CgiError::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(CgiError::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/cgi_error/error.rs b/src/client/connection/response/failure/temporary/cgi_error/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/cgi_error/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/default.rs b/src/client/connection/response/failure/temporary/default.rs new file mode 100644 index 0000000..e56d90b --- /dev/null +++ b/src/client/connection/response/failure/temporary/default.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code +pub const CODE: &[u8] = b"40"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Temporary error"; + +/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let d = Default::from_utf8("40 Message\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Message")); + assert_eq!(d.message_or_default(), "Message"); + assert_eq!(d.as_str(), "40 Message\r\n"); + assert_eq!(d.as_bytes(), "40 Message\r\n".as_bytes()); + + let d = Default::from_utf8("40\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "40\r\n"); + assert_eq!(d.as_bytes(), "40\r\n".as_bytes()); + + // err + assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/default/error.rs b/src/client/connection/response/failure/temporary/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/error.rs b/src/client/connection/response/failure/temporary/error.rs index 5cf1cf6..afa9154 100644 --- a/src/client/connection/response/failure/temporary/error.rs +++ b/src/client/connection/response/failure/temporary/error.rs @@ -1,22 +1,47 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + CgiError(super::cgi_error::Error), + Default(super::default::Error), + FirstByte(u8), + ProxyError(super::proxy_error::Error), + SecondByte(u8), + ServerUnavailable(super::server_unavailable::Error), + SlowDown(super::slow_down::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::CgiError(e) => { + write!(f, "CgiError parse error: {e}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") + } + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") + } + Self::ProxyError(e) => { + write!(f, "ProxyError parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::ServerUnavailable(e) => { + write!(f, "ServerUnavailable parse error: {e}") + } + Self::SlowDown(e) => { + write!(f, "SlowDown parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/failure/temporary/proxy_error.rs b/src/client/connection/response/failure/temporary/proxy_error.rs new file mode 100644 index 0000000..1264c34 --- /dev/null +++ b/src/client/connection/response/failure/temporary/proxy_error.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code +pub const CODE: &[u8] = b"43"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Proxy error"; + +/// Hold header `String` for [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ProxyError(String); + +impl ProxyError { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let pe = ProxyError::from_utf8("43 Message\r\n".as_bytes()).unwrap(); + assert_eq!(pe.message(), Some("Message")); + assert_eq!(pe.message_or_default(), "Message"); + assert_eq!(pe.as_str(), "43 Message\r\n"); + assert_eq!(pe.as_bytes(), "43 Message\r\n".as_bytes()); + + let pe = ProxyError::from_utf8("43\r\n".as_bytes()).unwrap(); + assert_eq!(pe.message(), None); + assert_eq!(pe.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(pe.as_str(), "43\r\n"); + assert_eq!(pe.as_bytes(), "43\r\n".as_bytes()); + + // err + assert!(ProxyError::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ProxyError::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ProxyError::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/proxy_error/error.rs b/src/client/connection/response/failure/temporary/proxy_error/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/proxy_error/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/server_unavailable.rs b/src/client/connection/response/failure/temporary/server_unavailable.rs new file mode 100644 index 0000000..f42802e --- /dev/null +++ b/src/client/connection/response/failure/temporary/server_unavailable.rs @@ -0,0 +1,81 @@ +pub mod error; +pub use error::Error; + +/// [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable) +/// temporary error status code +pub const CODE: &[u8] = b"41"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Server unavailable"; + +/// Hold header `String` for [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable) +/// temporary error status code +/// +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ServerUnavailable(String); + +impl ServerUnavailable { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let su = ServerUnavailable::from_utf8("41 Message\r\n".as_bytes()).unwrap(); + assert_eq!(su.message(), Some("Message")); + assert_eq!(su.message_or_default(), "Message"); + assert_eq!(su.as_str(), "41 Message\r\n"); + assert_eq!(su.as_bytes(), "41 Message\r\n".as_bytes()); + + let su = ServerUnavailable::from_utf8("41\r\n".as_bytes()).unwrap(); + assert_eq!(su.message(), None); + assert_eq!(su.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(su.as_str(), "41\r\n"); + assert_eq!(su.as_bytes(), "41\r\n".as_bytes()); + + // err + assert!(ServerUnavailable::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ServerUnavailable::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ServerUnavailable::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/server_unavailable/error.rs b/src/client/connection/response/failure/temporary/server_unavailable/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/server_unavailable/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/slow_down.rs b/src/client/connection/response/failure/temporary/slow_down.rs new file mode 100644 index 0000000..3ca346d --- /dev/null +++ b/src/client/connection/response/failure/temporary/slow_down.rs @@ -0,0 +1,81 @@ +pub mod error; +pub use error::Error; + +/// [Slow Down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down) +/// temporary error status code +pub const CODE: &[u8] = b"44"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Slow down"; + +/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down) +/// temporary error status code +/// +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct SlowDown(String); + +impl SlowDown { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let sd = SlowDown::from_utf8("44 Message\r\n".as_bytes()).unwrap(); + assert_eq!(sd.message(), Some("Message")); + assert_eq!(sd.message_or_default(), "Message"); + assert_eq!(sd.as_str(), "44 Message\r\n"); + assert_eq!(sd.as_bytes(), "44 Message\r\n".as_bytes()); + + let sd = SlowDown::from_utf8("44\r\n".as_bytes()).unwrap(); + assert_eq!(sd.message(), None); + assert_eq!(sd.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(sd.as_str(), "44\r\n"); + assert_eq!(sd.as_bytes(), "44\r\n".as_bytes()); + + // err + assert!(SlowDown::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(SlowDown::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(SlowDown::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/slow_down/error.rs b/src/client/connection/response/failure/temporary/slow_down/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/slow_down/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From 46da3a031af584bc235a0942cb07493b38022222 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 07:08:20 +0200 Subject: [PATCH 375/392] remove extras --- src/client/connection/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 840cf08..7dd54bb 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -74,7 +74,7 @@ impl Response { connection, ) }, - ); + ) } } From 8ee088270f5ba128a7dd766e57490f1778148501 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 07:36:48 +0200 Subject: [PATCH 376/392] implement high-level getters, add comments, improve tests --- src/client/connection/response/success.rs | 47 +++++++++++++++++-- .../connection/response/success/default.rs | 23 ++++++--- .../response/success/default/header.rs | 41 ++++++++++------ 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index ecee769..f9493d6 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -24,14 +24,53 @@ impl Success { Err(e) => Err(Error::Default(e)), } } + + // Getters + + /// Get header bytes for `Self` type + pub fn as_header_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.header.as_bytes(), + } + } + + /// Get header string for `Self` type + pub fn as_header_str(&self) -> &str { + match self { + Self::Default(default) => default.header.as_str(), + } + } + + /// Get parsed MIME for `Self` type + /// + /// * high-level method, useful to skip extra match case constructions; + /// * at this moment, Gemini protocol has only one status code in this scope,\ + /// this method would be deprecated in future, use on your own risk! + pub fn mime(&self) -> Result { + match self { + Self::Default(default) => default + .header + .mime() + .map_err(|e| Error::Default(default::Error::Header(e))), + } + } } #[test] fn test() { - match Success::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { - Success::Default(default) => { - assert_eq!(default.header.mime().unwrap(), "text/gemini"); - assert_eq!(default.content, None) + let r = "20 text/gemini; charset=utf-8; lang=en\r\n"; + let b = r.as_bytes(); + let s = Success::from_utf8(b).unwrap(); + + match s { + Success::Default(ref d) => { + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert!(d.content.is_empty()) } } + assert_eq!(s.as_header_bytes(), b); + assert_eq!(s.as_header_str(), r); + assert_eq!(s.mime().unwrap(), "text/gemini"); + + assert!(Success::from_utf8("40 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err()) } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index a905318..488c3e6 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -11,13 +11,18 @@ pub const CODE: &[u8] = b"20"; /// * this response type MAY contain body data /// * the header has closed members to require valid construction pub struct Default { + /// Formatted header holder with additional API pub header: Header, - pub content: Option>, + /// Default success response MAY include body data + /// * if the `Request` constructed with `Mode::HeaderOnly` flag,\ + /// this value wants to be processed manually, using external application logic (specific for content-type) + pub content: Vec, } impl Default { // Constructors + /// Parse `Self` from buffer contains header bytes pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(CODE) { return Err(Error::Code); @@ -25,9 +30,9 @@ impl Default { let header = Header::from_utf8(buffer).map_err(Error::Header)?; Ok(Self { content: buffer - .get(header.len() + 1..) + .get(header.as_bytes().len()..) .filter(|s| !s.is_empty()) - .map(|v| v.to_vec()), + .map_or(Vec::new(), |v| v.to_vec()), header, }) } @@ -35,8 +40,12 @@ impl Default { #[test] fn test() { - let default = - Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); - assert_eq!(default.header.mime().unwrap(), "text/gemini"); - assert_eq!(default.content, None) + let d = Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert!(d.content.is_empty()); + + let d = + Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\ndata".as_bytes()).unwrap(); + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert_eq!(d.content.len(), 4); } diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs index 929cb96..dab58b7 100644 --- a/src/client/connection/response/success/default/header.rs +++ b/src/client/connection/response/success/default/header.rs @@ -1,19 +1,22 @@ pub mod error; pub use error::Error; -pub struct Header(Vec); +pub struct Header(String); impl Header { // Constructors + /// Parse `Self` from buffer contains header bytes pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(super::CODE) { return Err(Error::Code); } Ok(Self( - crate::client::connection::response::header_bytes(buffer) - .map_err(Error::Header)? - .to_vec(), + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), )) } @@ -23,7 +26,7 @@ impl Header { pub fn mime(&self) -> Result { glib::Regex::split_simple( r"^\d{2}\s([^\/]+\/[^\s;]+)", - std::str::from_utf8(&self.0).map_err(Error::Utf8Error)?, + &self.0, glib::RegexCompileFlags::DEFAULT, glib::RegexMatchFlags::DEFAULT, ) @@ -33,15 +36,25 @@ impl Header { .map_or(Err(Error::Mime), |s| Ok(s.to_lowercase())) } - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { - &self.0 + self.0.as_bytes() + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + self.0.as_str() } } + +#[test] +fn test() { + let s = "20 text/gemini; charset=utf-8; lang=en\r\n"; + let b = s.as_bytes(); + let h = Header::from_utf8(b).unwrap(); + assert_eq!(h.mime().unwrap(), "text/gemini"); + assert_eq!(h.as_bytes(), b); + assert_eq!(h.as_str(), s); + + assert!(Header::from_utf8("21 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err()); +} From b6ea830545b6da6cb073cfb37a2bd6260e5c1622 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 07:38:54 +0200 Subject: [PATCH 377/392] update example --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3abf80f..d81850e 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,9 @@ fn main() -> ExitCode { None, // optional `GTlsCertificate` |result| match result { Ok((response, _connection)) => match response { - Response::Success(success) => match success { - Success::Default(default) => match default.header.mime().unwrap().as_str() { - "text/gemini" => todo!(), - _ => todo!(), - } + Response::Success(success) => match success.mime().unwrap().as_str() { + "text/gemini" => todo!(), + _ => todo!(), }, _ => todo!(), }, From 4dddbd5f8a0a5160690ede5997c4e22f36767721 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 27 Mar 2025 21:14:14 +0200 Subject: [PATCH 378/392] make Size struct for tuple argument --- src/gio/file_output_stream.rs | 57 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 7d9415c..c915b2f 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -7,6 +7,14 @@ use gio::{ }; use glib::{Bytes, Priority, object::IsA}; +/// Mutable bytes count +pub struct Size { + pub chunk: usize, + /// `None` for unlimited + pub limit: Option, + pub total: usize, +} + /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread @@ -15,59 +23,50 @@ pub fn from_stream_async( file_output_stream: FileOutputStream, cancellable: Cancellable, priority: Priority, - (chunk, limit, mut total): ( - usize, // bytes_in_chunk - Option, // bytes_total_limit, `None` to unlimited - usize, // bytes_total - ), + mut size: Size, (on_chunk, on_complete): ( impl Fn(Bytes, usize) + 'static, // on_chunk impl FnOnce(Result<(FileOutputStream, usize), Error>) + 'static, // on_complete ), ) { io_stream.input_stream().read_bytes_async( - chunk, + size.chunk, priority, Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - total += bytes.len(); - on_chunk(bytes.clone(), total); + size.total += bytes.len(); + on_chunk(bytes.clone(), size.total); - if let Some(limit) = limit { - if total > limit { - return on_complete(Err(Error::BytesTotal(total, limit))); + if let Some(limit) = size.limit { + if size.total > limit { + return on_complete(Err(Error::BytesTotal(size.total, limit))); } } if bytes.len() == 0 { - return on_complete(Ok((file_output_stream, total))); + return on_complete(Ok((file_output_stream, size.total))); } // Make sure **all bytes** sent to the destination // > 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 file_output_stream.clone().write_all_async( - bytes.clone(), + bytes, priority, Some(&cancellable.clone()), - move |result| { - match result { - Ok(_) => { - // continue read.. - from_stream_async( - io_stream, - file_output_stream, - cancellable, - priority, - (chunk, limit, total), - (on_chunk, on_complete), - ); - } - Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))), - } + move |result| match result { + Ok(_) => from_stream_async( + io_stream, + file_output_stream, + cancellable, + priority, + size, + (on_chunk, on_complete), + ), + Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))), }, - ); + ) } Err(e) => on_complete(Err(Error::InputStream(e))), }, From 9bbaecf344440496da4873b6988ce823aebc656e Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 27 Mar 2025 21:33:50 +0200 Subject: [PATCH 379/392] make Size struct for tuple argument, move Size to separated mod --- src/gio/file_output_stream.rs | 11 +++-------- src/gio/file_output_stream/size.rs | 7 +++++++ src/gio/memory_input_stream.rs | 27 +++++++++++++++++---------- src/gio/memory_input_stream/size.rs | 6 ++++++ 4 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 src/gio/file_output_stream/size.rs create mode 100644 src/gio/memory_input_stream/size.rs diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index c915b2f..a8e5d70 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -1,5 +1,8 @@ pub mod error; +pub mod size; + pub use error::Error; +pub use size::Size; use gio::{ Cancellable, FileOutputStream, IOStream, @@ -7,14 +10,6 @@ use gio::{ }; use glib::{Bytes, Priority, object::IsA}; -/// Mutable bytes count -pub struct Size { - pub chunk: usize, - /// `None` for unlimited - pub limit: Option, - pub total: usize, -} - /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread diff --git a/src/gio/file_output_stream/size.rs b/src/gio/file_output_stream/size.rs new file mode 100644 index 0000000..285d9f1 --- /dev/null +++ b/src/gio/file_output_stream/size.rs @@ -0,0 +1,7 @@ +/// Mutable bytes count +pub struct Size { + pub chunk: usize, + /// `None` for unlimited + pub limit: Option, + pub total: usize, +} diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 21c6337..2b1fc39 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -1,5 +1,8 @@ pub mod error; +pub mod size; + pub use error::Error; +pub use size::Size; use gio::{ Cancellable, IOStream, MemoryInputStream, @@ -17,7 +20,7 @@ pub fn from_stream_async( io_stream: impl IsA, priority: Priority, cancelable: Cancellable, - (chunk, limit): (usize, usize), + size: Size, (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, @@ -28,7 +31,7 @@ pub fn from_stream_async( io_stream, priority, cancelable, - (chunk, limit, 0), + size, (on_chunk, on_complete), ); } @@ -41,14 +44,14 @@ pub fn for_memory_input_stream_async( io_stream: impl IsA, priority: Priority, cancellable: Cancellable, - (chunk, limit, mut total): (usize, usize, usize), + mut size: Size, (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { io_stream.input_stream().read_bytes_async( - chunk, + size.chunk, priority, Some(&cancellable.clone()), move |result| match result { @@ -57,19 +60,23 @@ pub fn for_memory_input_stream_async( // is end of stream if len == 0 { - return on_complete(Ok((memory_input_stream, total))); + return on_complete(Ok((memory_input_stream, size.total))); } // callback chunk function - total += len; - on_chunk(len, total); + size.total += len; + on_chunk(len, size.total); // push bytes into the memory pool memory_input_stream.add_bytes(&bytes); // prevent memory overflow - if total > limit { - return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); + if size.total > size.limit { + return on_complete(Err(Error::BytesTotal( + memory_input_stream, + size.total, + size.limit, + ))); } // handle next chunk.. @@ -78,7 +85,7 @@ pub fn for_memory_input_stream_async( io_stream, priority, cancellable, - (chunk, limit, total), + size, (on_chunk, on_complete), ) } diff --git a/src/gio/memory_input_stream/size.rs b/src/gio/memory_input_stream/size.rs new file mode 100644 index 0000000..b95ef39 --- /dev/null +++ b/src/gio/memory_input_stream/size.rs @@ -0,0 +1,6 @@ +/// Mutable bytes count +pub struct Size { + pub chunk: usize, + pub limit: usize, + pub total: usize, +} From c79f386bf1c1c3e5eb02920adf143122358a7e01 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 28 Mar 2025 00:33:33 +0200 Subject: [PATCH 380/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 442d0ee..27d8051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.18.0" +version = "0.18.1" edition = "2024" license = "MIT" readme = "README.md" From bb5b1dfb533ded64b0eb02d1cdecb19bbd7d40a9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Jul 2025 08:44:50 +0300 Subject: [PATCH 381/392] apply clippy optimizations --- src/gio/file_output_stream.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index a8e5d70..777600f 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -39,7 +39,7 @@ pub fn from_stream_async( } } - if bytes.len() == 0 { + if bytes.is_empty() { return on_complete(Ok((file_output_stream, size.total))); } From 44196608cebe51213c1feb4cc53bc8557bc16f72 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Jul 2025 08:48:56 +0300 Subject: [PATCH 382/392] implement optional TOFU validation --- Cargo.toml | 2 +- README.md | 3 ++- src/client.rs | 6 ++++-- src/client/connection.rs | 27 +++++++++++++++++++-------- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 27d8051..1c6ea42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.18.1" +version = "0.19.0" edition = "2024" license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index d81850e..03c633e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ fn main() -> ExitCode { }, Priority::DEFAULT, Cancellable::new(), - None, // optional `GTlsCertificate` + None, // optional auth `GTlsCertificate` + None, // optional TOFU `GTlsCertificate` array |result| match result { Ok((response, _connection)) => match response { Response::Success(success) => match success.mime().unwrap().as_str() { diff --git a/src/client.rs b/src/client.rs index cff557e..f11e9ec 100644 --- a/src/client.rs +++ b/src/client.rs @@ -59,7 +59,8 @@ impl Client { request: Request, priority: Priority, cancellable: Cancellable, - certificate: Option, + client_certificate: Option, + server_certificates: Option>, callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static, ) { // Begin new connection @@ -75,7 +76,8 @@ impl Client { match Connection::build( socket_connection, network_address, - certificate, + client_certificate, + server_certificates, is_session_resumption, ) { Ok(connection) => connection.request_async( diff --git a/src/client/connection.rs b/src/client/connection.rs index d1cd849..b7833a7 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -6,11 +6,9 @@ pub use error::Error; pub use request::{Mode, Request}; pub use response::Response; -// Local dependencies - use gio::{ Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, - prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExtManual, TlsCertificateExt, TlsConnectionExt}, }; use glib::{ Bytes, Priority, @@ -30,17 +28,19 @@ impl Connection { pub fn build( socket_connection: SocketConnection, network_address: NetworkAddress, - certificate: Option, + client_certificate: Option, + server_certificates: Option>, is_session_resumption: bool, ) -> Result { Ok(Self { tls_client_connection: match new_tls_client_connection( &socket_connection, Some(&network_address), + server_certificates, is_session_resumption, ) { Ok(tls_client_connection) => { - if let Some(ref c) = certificate { + if let Some(ref c) = client_certificate { tls_client_connection.set_certificate(c); } tls_client_connection @@ -136,6 +136,7 @@ impl Connection { fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, + server_certificates: Option>, is_session_resumption: bool, ) -> Result { match TlsClientConnection::new(socket_connection, server_identity) { @@ -149,9 +150,19 @@ fn new_tls_client_connection( // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections tls_client_connection.set_require_close_notify(true); - // @TODO validate - // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation - tls_client_connection.connect_accept_certificate(|_, _, _| true); + // [TOFU](https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation) + tls_client_connection.connect_accept_certificate(move |_, c, _| { + server_certificates + .as_ref() + .is_none_or(|server_certificates| { + for server_certificate in server_certificates { + if server_certificate.is_same(c) { + return true; + } + } + false + }) + }); Ok(tls_client_connection) } From cc1018224a42b3d63e9cc77db86102710e79435a Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Jul 2025 09:46:32 +0300 Subject: [PATCH 383/392] reorganize error types, return `socket_connection` on init error --- src/client.rs | 10 ++++++---- src/client/error.rs | 16 ++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index f11e9ec..0f33a94 100644 --- a/src/client.rs +++ b/src/client.rs @@ -74,7 +74,7 @@ impl Client { move |result| match result { Ok(socket_connection) => { match Connection::build( - socket_connection, + socket_connection.clone(), network_address, client_certificate, server_certificates, @@ -87,18 +87,20 @@ impl Client { move |result| { callback(match result { Ok(response) => Ok(response), - Err(e) => Err(Error::Connection(e)), + Err(e) => Err(Error::Request(e)), }) }, ), - Err(e) => callback(Err(Error::Connection(e))), + Err(e) => { + callback(Err(Error::Connection(socket_connection, e))) + } } } Err(e) => callback(Err(Error::Connect(e))), } }) } - Err(e) => callback(Err(Error::Request(e))), + Err(e) => callback(Err(Error::NetworkAddress(e))), } } diff --git a/src/client/error.rs b/src/client/error.rs index 6083e77..b49d65a 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -3,21 +3,25 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { Connect(glib::Error), - Connection(crate::client::connection::Error), - Request(crate::client::connection::request::Error), + Connection(gio::SocketConnection, crate::client::connection::Error), + NetworkAddress(crate::client::connection::request::Error), + Request(crate::client::connection::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Connection(e) => { - write!(f, "Connection error: {e}") - } Self::Connect(e) => { write!(f, "Connect error: {e}") } + Self::Connection(_, e) => { + write!(f, "Connection init error: {e}") + } + Self::NetworkAddress(e) => { + write!(f, "Network address error: {e}") + } Self::Request(e) => { - write!(f, "Request error: {e}") + write!(f, "Connection error: {e}") } } } From e878fe4ba2d7b73816dd3e4be90411aa401ab272 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Jul 2025 09:49:39 +0300 Subject: [PATCH 384/392] return `NetworkAddress` on `Error::Connect` --- src/client.rs | 2 +- src/client/error.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0f33a94..5198f81 100644 --- a/src/client.rs +++ b/src/client.rs @@ -96,7 +96,7 @@ impl Client { } } } - Err(e) => callback(Err(Error::Connect(e))), + Err(e) => callback(Err(Error::Connect(network_address, e))), } }) } diff --git a/src/client/error.rs b/src/client/error.rs index b49d65a..eb951b6 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Connect(glib::Error), + Connect(gio::NetworkAddress, glib::Error), Connection(gio::SocketConnection, crate::client::connection::Error), NetworkAddress(crate::client::connection::request::Error), Request(crate::client::connection::Error), @@ -11,7 +11,7 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Connect(e) => { + Self::Connect(_, e) => { write!(f, "Connect error: {e}") } Self::Connection(_, e) => { From c5d10e020a1d246aa1bd5ae521cde03f06155e6d Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 01:27:20 +0300 Subject: [PATCH 385/392] return `Connection` on `Request` error --- src/client.rs | 4 ++-- src/client/connection.rs | 1 + src/client/error.rs | 7 +++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5198f81..2152781 100644 --- a/src/client.rs +++ b/src/client.rs @@ -80,14 +80,14 @@ impl Client { server_certificates, is_session_resumption, ) { - Ok(connection) => connection.request_async( + Ok(connection) => connection.clone().request_async( request, priority, cancellable, move |result| { callback(match result { Ok(response) => Ok(response), - Err(e) => Err(Error::Request(e)), + Err(e) => Err(Error::Request(connection, e)), }) }, ), diff --git a/src/client/connection.rs b/src/client/connection.rs index b7833a7..6be90f1 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -15,6 +15,7 @@ use glib::{ object::{Cast, ObjectExt}, }; +#[derive(Debug, Clone)] pub struct Connection { pub network_address: NetworkAddress, pub socket_connection: SocketConnection, diff --git a/src/client/error.rs b/src/client/error.rs index eb951b6..73031da 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -5,7 +5,10 @@ pub enum Error { Connect(gio::NetworkAddress, glib::Error), Connection(gio::SocketConnection, crate::client::connection::Error), NetworkAddress(crate::client::connection::request::Error), - Request(crate::client::connection::Error), + Request( + crate::client::connection::Connection, + crate::client::connection::Error, + ), } impl Display for Error { @@ -20,7 +23,7 @@ impl Display for Error { Self::NetworkAddress(e) => { write!(f, "Network address error: {e}") } - Self::Request(e) => { + Self::Request(_, e) => { write!(f, "Connection error: {e}") } } From d8e0a8e35a136ee4b32e3bb1c38979539c0d8044 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 03:07:24 +0300 Subject: [PATCH 386/392] update dependencies version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1c6ea42..7c12b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.20.9" +version = "0.21" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.20.9" +version = "0.21" features = ["v2_66"] From 5019e6666772b8d06823339176094db9062167f9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 04:29:01 +0300 Subject: [PATCH 387/392] use latest 0.20 api --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7c12b4a..0e27134 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.21" +version = "0.20.12" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.21" +version = "0.20.12" features = ["v2_66"] From f8537e4ab63cf07992f12c909ea9480d541df35d Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 05:08:13 +0300 Subject: [PATCH 388/392] use latest dependencies version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0e27134..901e1b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.20.12" +version = "0.21.0" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.20.12" +version = "0.21.0" features = ["v2_66"] From 7e9ecf64b3c5e456f739413892d14b0c5c83dda2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Oct 2025 22:37:48 +0300 Subject: [PATCH 389/392] implement default trait --- src/gio/file_output_stream/size.rs | 10 ++++++++++ src/gio/memory_input_stream/size.rs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/gio/file_output_stream/size.rs b/src/gio/file_output_stream/size.rs index 285d9f1..5d0c911 100644 --- a/src/gio/file_output_stream/size.rs +++ b/src/gio/file_output_stream/size.rs @@ -5,3 +5,13 @@ pub struct Size { pub limit: Option, pub total: usize, } + +impl Default for Size { + fn default() -> Self { + Self { + chunk: 0x10000, // 64KB + limit: None, + total: 0, + } + } +} diff --git a/src/gio/memory_input_stream/size.rs b/src/gio/memory_input_stream/size.rs index b95ef39..9a10bd3 100644 --- a/src/gio/memory_input_stream/size.rs +++ b/src/gio/memory_input_stream/size.rs @@ -4,3 +4,13 @@ pub struct Size { pub limit: usize, pub total: usize, } + +impl Default for Size { + fn default() -> Self { + Self { + chunk: 0x10000, // 64KB + limit: 0xfffff, // 1 MB + total: 0, + } + } +} From 0f6eaa563c428f85ecd49c9f562b2987b6fed317 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Oct 2025 22:38:51 +0300 Subject: [PATCH 390/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 901e1b7..12c3c6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.19.0" +version = "0.20.0" edition = "2024" license = "MIT" readme = "README.md" From bba51e38e831cc84060c901e1dbe2e6efa0823ad Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Oct 2025 22:46:49 +0300 Subject: [PATCH 391/392] apply fmt updates --- src/gio/file_output_stream.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 777600f..2dffb5e 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -33,10 +33,10 @@ pub fn from_stream_async( size.total += bytes.len(); on_chunk(bytes.clone(), size.total); - if let Some(limit) = size.limit { - if size.total > limit { - return on_complete(Err(Error::BytesTotal(size.total, limit))); - } + if let Some(limit) = size.limit + && size.total > limit + { + return on_complete(Err(Error::BytesTotal(size.total, limit))); } if bytes.is_empty() { From 11d17e004e473354bc0d9ca9ae701af6cd281ee7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 7 Nov 2025 21:15:20 +0200 Subject: [PATCH 392/392] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 12c3c6c..801fb2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.20.0" +version = "0.20.1" edition = "2024" license = "MIT" readme = "README.md"