mirror of
https://github.com/YGGverse/ggemini.git
synced 2026-03-31 09:05:45 +00:00
reorganize redirection structs format: make constructors lazy, parse members on get
This commit is contained in:
parent
473ed48715
commit
5229cdae85
6 changed files with 414 additions and 213 deletions
|
|
@ -1,16 +1,23 @@
|
||||||
pub mod error;
|
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");
|
// Local dependencies
|
||||||
const PERMANENT: (u8, &str) = (31, "Permanent redirect");
|
|
||||||
|
|
||||||
|
use glib::{Uri, UriFlags};
|
||||||
|
|
||||||
|
const CODE: u8 = b'3';
|
||||||
|
|
||||||
|
/// [Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) statuses
|
||||||
pub enum Redirect {
|
pub enum Redirect {
|
||||||
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
|
/// 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
|
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
|
||||||
Permanent { target: String },
|
Permanent(Permanent),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Redirect {
|
impl Redirect {
|
||||||
|
|
@ -18,210 +25,161 @@ impl Redirect {
|
||||||
|
|
||||||
/// Create new `Self` from buffer include header bytes
|
/// Create new `Self` from buffer include header bytes
|
||||||
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
|
||||||
use std::str::FromStr;
|
match buffer.first() {
|
||||||
match std::str::from_utf8(buffer) {
|
Some(b) => match *b {
|
||||||
Ok(header) => Self::from_str(header),
|
CODE => match buffer.get(1) {
|
||||||
Err(e) => Err(Error::Utf8Error(e)),
|
Some(b) => match *b {
|
||||||
}
|
b'0' => Ok(Self::Temporary(
|
||||||
}
|
Temporary::from_utf8(buffer).map_err(Error::Temporary)?,
|
||||||
|
)),
|
||||||
// Convertors
|
b'1' => Ok(Self::Permanent(
|
||||||
|
Permanent::from_utf8(buffer).map_err(Error::Permanent)?,
|
||||||
pub fn to_code(&self) -> u8 {
|
)),
|
||||||
match self {
|
b => Err(Error::SecondByte(b)),
|
||||||
Self::Permanent { .. } => PERMANENT,
|
},
|
||||||
Self::Temporary { .. } => TEMPORARY,
|
None => Err(Error::UndefinedSecondByte),
|
||||||
}
|
},
|
||||||
.0
|
b => Err(Error::FirstByte(b)),
|
||||||
}
|
|
||||||
|
|
||||||
/// 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> {
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
UriFlags::NONE,
|
None => Err(Error::UndefinedFirstByte),
|
||||||
) {
|
|
||||||
Ok(absolute) => Ok(absolute),
|
|
||||||
Err(e) => Err(Error::Uri(e)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|
||||||
pub fn target(&self) -> &str {
|
pub fn target(&self) -> Result<&str, Error> {
|
||||||
match self {
|
match self {
|
||||||
Self::Permanent { target } => target,
|
Self::Temporary(temporary) => temporary.target().map_err(Error::Temporary),
|
||||||
Self::Temporary { target } => target,
|
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<Uri, Error> {
|
||||||
|
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 {
|
// Tools
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(
|
/// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection),
|
||||||
f,
|
/// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base`
|
||||||
"{}",
|
/// * fragment implementation uncompleted @TODO
|
||||||
match self {
|
fn uri(target: &str, base: &Uri) -> Result<Uri, UriError> {
|
||||||
Self::Permanent { .. } => PERMANENT,
|
match Uri::build(
|
||||||
Self::Temporary { .. } => TEMPORARY,
|
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
|
},
|
||||||
)
|
UriFlags::NONE,
|
||||||
}
|
) {
|
||||||
}
|
Ok(absolute) => Ok(absolute),
|
||||||
|
Err(e) => Err(UriError::ParseRelative(e)),
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test() {
|
fn test() {
|
||||||
use std::str::FromStr;
|
/// Test common assertion rules
|
||||||
{
|
fn t(base: &Uri, source: &str, target: &str) {
|
||||||
let temporary = Redirect::from_str("30 /uri\r\n").unwrap();
|
let b = source.as_bytes();
|
||||||
assert_eq!(temporary.target(), "/uri");
|
let r = Redirect::from_utf8(b).unwrap();
|
||||||
assert_eq!(temporary.to_code(), TEMPORARY.0);
|
assert!(r.uri(base).is_ok_and(|u| u.to_string() == target));
|
||||||
assert_eq!(temporary.to_string(), TEMPORARY.1);
|
assert_eq!(r.as_str(), source);
|
||||||
|
assert_eq!(r.as_bytes(), b);
|
||||||
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/"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
// 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/");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,55 @@
|
||||||
use std::{
|
use std::fmt::{Display, Formatter, Result};
|
||||||
fmt::{Display, Formatter, Result},
|
|
||||||
str::Utf8Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
BaseHost,
|
FirstByte(u8),
|
||||||
Uri(glib::Error),
|
Permanent(super::permanent::Error),
|
||||||
Protocol,
|
SecondByte(u8),
|
||||||
Target,
|
Temporary(super::temporary::Error),
|
||||||
Utf8Error(Utf8Error),
|
UndefinedFirstByte,
|
||||||
|
UndefinedSecondByte,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
match self {
|
match self {
|
||||||
Self::BaseHost => {
|
Self::FirstByte(b) => {
|
||||||
write!(f, "Base host required")
|
write!(f, "Unexpected first byte: {b}")
|
||||||
}
|
}
|
||||||
Self::Uri(e) => {
|
Self::Permanent(e) => {
|
||||||
write!(f, "URI error: {e}")
|
write!(f, "Permanent parse error: {e}")
|
||||||
}
|
}
|
||||||
Self::Utf8Error(e) => {
|
Self::SecondByte(b) => {
|
||||||
write!(f, "UTF-8 error: {e}")
|
write!(f, "Unexpected second byte: {b}")
|
||||||
}
|
}
|
||||||
Self::Protocol => {
|
Self::Temporary(e) => {
|
||||||
write!(f, "Protocol error")
|
write!(f, "Temporary parse error: {e}")
|
||||||
}
|
}
|
||||||
Self::Target => {
|
Self::UndefinedFirstByte => {
|
||||||
write!(f, "Target error")
|
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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
79
src/client/connection/response/redirect/permanent.rs
Normal file
79
src/client/connection/response/redirect/permanent.rs
Normal 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());
|
||||||
|
}
|
||||||
32
src/client/connection/response/redirect/permanent/error.rs
Normal file
32
src/client/connection/response/redirect/permanent/error.rs
Normal 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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/client/connection/response/redirect/temporary.rs
Normal file
79
src/client/connection/response/redirect/temporary.rs
Normal 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())
|
||||||
|
}
|
||||||
32
src/client/connection/response/redirect/temporary/error.rs
Normal file
32
src/client/connection/response/redirect/temporary/error.rs
Normal 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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue