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

This commit is contained in:
yggverse 2025-03-24 22:50:03 +02:00
parent 1b96270598
commit 232531a0bc
8 changed files with 345 additions and 91 deletions

View file

@ -1,19 +1,24 @@
pub mod error; 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"); pub use error::Error;
const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); pub use not_authorized::NotAuthorized;
const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); pub use not_valid::NotValid;
pub use required::Required;
const CODE: u8 = b'6';
/// 6* status code group /// 6* status code group
/// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates
pub enum Certificate { pub enum Certificate {
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
Required { message: Option<String> }, Required(Required),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
NotAuthorized { message: Option<String> }, NotAuthorized(NotAuthorized),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
NotValid { message: Option<String> }, NotValid(NotValid),
} }
impl Certificate { impl Certificate {
@ -21,95 +26,72 @@ impl Certificate {
/// 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::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 // 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> { pub fn message(&self) -> Option<&str> {
match self { match self {
Self::Required { message } => message, Self::Required(required) => required.message(),
Self::NotAuthorized { message } => message, Self::NotAuthorized(not_authorized) => not_authorized.message(),
Self::NotValid { message } => message, Self::NotValid(not_valid) => not_valid.message(),
} }
.as_deref()
} }
}
impl std::fmt::Display for Certificate { pub fn as_str(&self) -> &str {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self {
write!( Self::Required(required) => required.as_str(),
f, Self::NotAuthorized(not_authorized) => not_authorized.as_str(),
"{}", Self::NotValid(not_valid) => not_valid.as_str(),
match self { }
Self::Required { .. } => REQUIRED,
Self::NotAuthorized { .. } => NOT_AUTHORIZED,
Self::NotValid { .. } => NOT_VALID,
}
.1
)
} }
}
impl std::str::FromStr for Certificate { pub fn as_bytes(&self) -> &[u8] {
type Err = Error; match self {
fn from_str(header: &str) -> Result<Self, Self::Err> { Self::Required(required) => required.as_bytes(),
if let Some(postfix) = header.strip_prefix("60") { Self::NotAuthorized(not_authorized) => not_authorized.as_bytes(),
return Ok(Self::Required { Self::NotValid(not_valid) => not_valid.as_bytes(),
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<String> {
let value = value.trim();
if value.is_empty() {
None
} else {
Some(value.to_string())
} }
} }
#[test] #[test]
fn test_from_str() { fn test() {
use std::str::FromStr; fn t(source: &str, message: Option<&str>) {
let b = source.as_bytes();
let required = Certificate::from_str("60 Message\r\n").unwrap(); let c = Certificate::from_utf8(b).unwrap();
assert_eq!(c.message(), message);
assert_eq!(required.message(), Some("Message")); assert_eq!(c.as_str(), source);
assert_eq!(required.to_code(), REQUIRED.0); assert_eq!(c.as_bytes(), b);
assert_eq!(required.to_string(), REQUIRED.1); }
// 60
let required = Certificate::from_str("60\r\n").unwrap(); t("60 Required\r\n", Some("Required"));
t("60\r\n", None);
assert_eq!(required.message(), None); // 61
assert_eq!(required.to_code(), REQUIRED.0); t("61 Not Authorized\r\n", Some("Not Authorized"));
assert_eq!(required.to_string(), REQUIRED.1); t("61\r\n", None);
// 62
t("61 Not Valid\r\n", Some("Not Valid"));
t("61\r\n", None);
} }

View file

@ -1,22 +1,39 @@
use std::{ use std::fmt::{Display, Formatter, Result};
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Code, FirstByte(u8),
Utf8Error(Utf8Error), NotAuthorized(super::not_authorized::Error),
NotValid(super::not_valid::Error),
Required(super::required::Error),
SecondByte(u8),
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::Code => { Self::FirstByte(b) => {
write!(f, "Status code error") write!(f, "Unexpected first byte: {b}")
} }
Self::Utf8Error(e) => { Self::NotAuthorized(e) => {
write!(f, "UTF-8 error: {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")
} }
} }
} }

View file

@ -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<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 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());
}

View file

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

View file

@ -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<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 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());
}

View file

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

View file

@ -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<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 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());
}

View file

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