reorganize redirection structs format: make constructors lazy, parse members on get

This commit is contained in:
yggverse 2025-03-25 02:18:02 +02:00
parent 473ed48715
commit 5229cdae85
6 changed files with 414 additions and 213 deletions

View file

@ -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,27 +25,63 @@ impl Redirect {
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
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::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)),
},
None => Err(Error::UndefinedFirstByte),
}
}
// Convertors
// Getters
pub fn to_code(&self) -> u8 {
pub fn target(&self) -> Result<&str, Error> {
match self {
Self::Permanent { .. } => PERMANENT,
Self::Temporary { .. } => TEMPORARY,
Self::Temporary(temporary) => temporary.target().map_err(Error::Temporary),
Self::Permanent(permanent) => permanent.target().map_err(Error::Permanent),
}
.0
}
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<Uri, Error> {
match self {
Self::Temporary(temporary) => temporary.uri(base).map_err(Error::Temporary),
Self::Permanent(permanent) => permanent.uri(base).map_err(Error::Permanent),
}
}
}
// 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
pub fn to_uri(&self, base: &Uri) -> Result<Uri, Error> {
fn uri(target: &str, base: &Uri) -> Result<Uri, UriError> {
match Uri::build(
UriFlags::NONE,
base.scheme().as_str(),
@ -58,7 +101,7 @@ impl Redirect {
&{
// 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();
let t = target;
match t.strip_prefix("//") {
Some(p) => {
let postfix = p.trim_start_matches(":");
@ -68,7 +111,7 @@ impl Redirect {
if postfix.is_empty() {
match base.host() {
Some(h) => format!("{h}/"),
None => return Err(Error::BaseHost),
None => return Err(UriError::BaseHost),
}
} else {
postfix.to_string()
@ -81,90 +124,21 @@ impl Redirect {
UriFlags::NONE,
) {
Ok(absolute) => Ok(absolute),
Err(e) => Err(Error::Uri(e)),
}
}
// 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 { .. } => PERMANENT,
Self::Temporary { .. } => TEMPORARY,
}
.1
)
}
}
impl std::str::FromStr for Redirect {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
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<String, Error> {
match value {
Some(target) => {
let target = target.trim();
if target.is_empty() {
Err(Error::Target)
} else {
Ok(target.to_string())
}
}
None => Err(Error::Target),
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);
/// 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",
@ -175,53 +149,37 @@ fn test() {
Some("query"),
Some("fragment"),
);
assert_eq!(
Redirect::from_str("30 /uri\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://geminiprotocol.net/uri"
// codes test
t(
&base,
"30 gemini://geminiprotocol.net/path\r\n",
"gemini://geminiprotocol.net/path",
);
assert_eq!(
Redirect::from_str("30 uri\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://geminiprotocol.net/path/uri"
t(
&base,
"31 gemini://geminiprotocol.net/path\r\n",
"gemini://geminiprotocol.net/path",
);
assert_eq!(
Redirect::from_str("30 gemini://test.host/uri\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://test.host/uri"
// relative test
t(
&base,
"31 path\r\n",
"gemini://geminiprotocol.net/path/path",
);
assert_eq!(
Redirect::from_str("30 //\r\n")
.unwrap()
.to_uri(&base)
.unwrap()
.to_string(),
"gemini://geminiprotocol.net/"
t(
&base,
"31 //geminiprotocol.net\r\n",
"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/"
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/");
}

View file

@ -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::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::Permanent(e) => {
write!(f, "Permanent parse error: {e}")
}
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::Temporary(e) => {
write!(f, "Temporary parse error: {e}")
}
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, "Base host required")
write!(f, "URI base host required")
}
Self::Uri(e) => {
write!(f, "URI error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 error: {e}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
Self::Target => {
write!(f, "Target error")
Self::ParseRelative(e) => {
write!(f, "URI parse relative error: {e}")
}
}
}

View file

@ -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<Self, Error> {
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<Uri, Error> {
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());
}

View file

@ -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}")
}
}
}
}

View file

@ -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<Self, Error> {
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<Uri, Error> {
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())
}

View file

@ -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}")
}
}
}
}