From 5229cdae858ebe210c28ab3b1eb19f46da62a90e Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 02:18:02 +0200 Subject: [PATCH] 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}") + } + } + } +}