implement Response api

This commit is contained in:
yggverse 2025-02-21 04:31:21 +02:00
parent 20284a7726
commit 07aae35a0e
28 changed files with 2292 additions and 1 deletions

View file

@ -1,2 +1,5 @@
pub mod request; pub mod request;
pub mod response;
pub use request::Request; pub use request::Request;
pub use response::Response;

View file

@ -104,7 +104,7 @@ impl<'a> Titan<'a> {
} }
#[test] #[test]
fn test_bytes() { fn test() {
const DATA: &[u8] = &[1, 2, 3]; const DATA: &[u8] = &[1, 2, 3];
const MIME: &str = "plain/text"; const MIME: &str = "plain/text";
const TOKEN: &str = "token"; const TOKEN: &str = "token";

366
src/response.rs Normal file
View file

@ -0,0 +1,366 @@
pub mod certificate;
pub mod failure;
pub mod input;
pub mod redirect;
pub mod success;
pub use certificate::Certificate;
pub use failure::Failure;
pub use input::Input;
pub use redirect::Redirect;
pub use success::Success;
use anyhow::{bail, Result};
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) source
pub enum Response {
Certificate(Certificate),
Failure(Failure),
Input(Input),
Redirect(Redirect),
Success(Success),
}
impl Response {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
match buffer.first() {
Some(byte) => Ok(match byte {
b'1' => Self::Input(Input::from_bytes(buffer)?),
b'2' => Self::Success(Success::from_bytes(buffer)?),
b'3' => Self::Redirect(Redirect::from_bytes(buffer)?),
b'4' | b'5' => Self::Failure(Failure::from_bytes(buffer)?),
b'6' => Self::Certificate(Certificate::from_bytes(buffer)?),
b => bail!("Unspecified header byte: {b}"),
}),
None => bail!("Empty source"),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::Certificate(this) => this.into_bytes(),
Self::Failure(this) => this.into_bytes(),
Self::Input(this) => this.into_bytes(),
Self::Redirect(this) => this.into_bytes(),
Self::Success(this) => this.into_bytes(),
}
}
}
#[test]
fn test() {
// 10
{
let source = format!("10 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Input(ref this) => match this {
Input::Default(this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
{
let source = format!("11 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Input(ref this) => match this {
Input::Sensitive(this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 20
{
let source = format!("20 text/gemini\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Success(ref this) => match this {
Success::Default(this) => assert_eq!(this.mime, "text/gemini".to_string()),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 30
{
let source = format!("30 target\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Redirect(ref this) => match this {
Redirect::Temporary(this) => assert_eq!(this.target, "target".to_string()),
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 31
{
let source = format!("31 target\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Redirect(ref this) => match this {
Redirect::Permanent(this) => assert_eq!(this.target, "target".to_string()),
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 4*
{
use failure::Temporary;
// 40
{
let source = format!("40 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Temporary(this) => match this {
Temporary::General(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 41
{
let source = format!("41 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Temporary(this) => match this {
Temporary::ServerUnavailable(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 42
{
let source = format!("42 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Temporary(this) => match this {
Temporary::CgiError(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 43
{
let source = format!("43 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Temporary(this) => match this {
Temporary::ProxyError(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 44
{
let source = format!("44 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Temporary(this) => match this {
Temporary::SlowDown(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
}
// 5*
{
use failure::Permanent;
// 50
{
let source = format!("50 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Permanent(this) => match this {
Permanent::General(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 51
{
let source = format!("51 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Permanent(this) => match this {
Permanent::NotFound(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 52
{
let source = format!("52 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Permanent(this) => match this {
Permanent::Gone(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 53
{
let source = format!("53 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Permanent(this) => match this {
Permanent::ProxyRequestRefused(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 59
{
let source = format!("59 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Failure(ref this) => match this {
Failure::Permanent(this) => match this {
Permanent::BadRequest(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
}
// 60
{
let source = format!("60 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Certificate(ref this) => match this {
Certificate::Expected(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 61
{
let source = format!("61 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Certificate(ref this) => match this {
Certificate::NotAuthorized(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 62
{
let source = format!("62 message\r\n");
let target = Response::from_bytes(source.as_bytes()).unwrap();
match target {
Response::Certificate(ref this) => match this {
Certificate::NotValid(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
}

View file

@ -0,0 +1,75 @@
pub mod expected;
pub mod not_authorized;
pub mod not_valid;
pub use expected::Expected;
pub use not_authorized::NotAuthorized;
pub use not_valid::NotValid;
use anyhow::{bail, Result};
/// [Client certificates](https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates)
pub enum Certificate {
Expected(Expected),
NotAuthorized(NotAuthorized),
NotValid(NotValid),
}
impl Certificate {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
if buffer.first().is_none_or(|b| *b != b'6') {
bail!("Unexpected first byte")
}
match buffer.get(1) {
Some(byte) => Ok(match byte {
b'0' => Self::Expected(Expected::from_bytes(buffer)?),
b'1' => Self::NotAuthorized(NotAuthorized::from_bytes(buffer)?),
b'2' => Self::NotValid(NotValid::from_bytes(buffer)?),
b => bail!("Unexpected second byte: {b}"),
}),
None => bail!("Invalid request"),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::Expected(this) => this.into_bytes(),
Self::NotAuthorized(this) => this.into_bytes(),
Self::NotValid(this) => this.into_bytes(),
}
}
}
#[test]
fn test() {
// 60
let request = format!("60 message\r\n");
let source = Certificate::from_bytes(request.as_bytes()).unwrap();
match source {
Certificate::Expected(ref this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
// 61
let request = format!("61 message\r\n");
let source = Certificate::from_bytes(request.as_bytes()).unwrap();
match source {
Certificate::NotAuthorized(ref this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
// 62
let request = format!("62 message\r\n");
let source = Certificate::from_bytes(request.as_bytes()).unwrap();
match source {
Certificate::NotValid(ref this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"60";
/// [Certificate expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60)
pub struct Expected {
pub message: Option<String>,
}
impl Expected {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("60 message\r\n");
let source = Expected::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"61";
/// [Certificate authorization](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized)
pub struct NotAuthorized {
pub message: Option<String>,
}
impl NotAuthorized {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("61 message\r\n");
let source = NotAuthorized::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"62";
/// [Certificate invalid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid)
pub struct NotValid {
pub message: Option<String>,
}
impl NotValid {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("62 message\r\n");
let source = NotValid::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

203
src/response/failure.rs Normal file
View file

@ -0,0 +1,203 @@
pub mod permanent;
pub mod temporary;
pub use permanent::Permanent;
pub use temporary::Temporary;
use anyhow::{bail, Result};
pub enum Failure {
/// [Permanent failure](https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure)
Permanent(Permanent),
/// [Temporary failure](https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure)
Temporary(Temporary),
}
impl Failure {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
match buffer.first() {
Some(byte) => Ok(match byte {
b'4' => Self::Temporary(Temporary::from_bytes(buffer)?),
b'5' => Self::Permanent(Permanent::from_bytes(buffer)?),
b => bail!("Unexpected first byte: {b}"),
}),
None => bail!("Invalid request"),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::Temporary(this) => this.into_bytes(),
Self::Permanent(this) => this.into_bytes(),
}
}
}
#[test]
fn test() {
// 4*
{
// 40
{
let request = format!("40 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Temporary(ref this) => match this {
Temporary::General(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
// 41
{
let request = format!("41 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Temporary(ref this) => match this {
Temporary::ServerUnavailable(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
// 42
{
let request = format!("42 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Temporary(ref this) => match this {
Temporary::CgiError(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
// 43
{
let request = format!("43 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Temporary(ref this) => match this {
Temporary::ProxyError(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
// 44
{
let request = format!("44 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Temporary(ref this) => match this {
Temporary::SlowDown(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
}
// 5*
{
// 50
{
let request = format!("50 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Permanent(ref this) => match this {
Permanent::General(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
// 51
{
let request = format!("51 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Permanent(ref this) => match this {
Permanent::NotFound(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
// 52
{
let request = format!("52 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Permanent(ref this) => match this {
Permanent::Gone(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
// 53
{
let request = format!("53 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Permanent(ref this) => match this {
Permanent::ProxyRequestRefused(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
// 59
{
let request = format!("59 message\r\n");
let source = Failure::from_bytes(request.as_bytes()).unwrap();
match source {
Failure::Permanent(ref this) => match this {
Permanent::BadRequest(this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
},
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}
}
}

View file

@ -0,0 +1,118 @@
pub mod bad_request;
pub mod general;
pub mod gone;
pub mod not_found;
pub mod proxy_request_refused;
pub use bad_request::BadRequest;
pub use general::General;
pub use gone::Gone;
pub use not_found::NotFound;
pub use proxy_request_refused::ProxyRequestRefused;
use anyhow::{bail, Result};
/// [Permanent failure](https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure)
pub enum Permanent {
/// [General permanent failure](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50)
General(General),
/// [Not found](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found)
NotFound(NotFound),
/// [Gone](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone)
Gone(Gone),
/// [Proxy request refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused)
ProxyRequestRefused(ProxyRequestRefused),
/// [Bad request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request)
BadRequest(BadRequest),
}
impl Permanent {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
if buffer.first().is_none_or(|b| *b != b'5') {
bail!("Unexpected first byte")
}
match buffer.get(1) {
Some(byte) => Ok(match byte {
b'0' => Self::General(General::from_bytes(buffer)?),
b'1' => Self::NotFound(NotFound::from_bytes(buffer)?),
b'2' => Self::Gone(Gone::from_bytes(buffer)?),
b'3' => Self::ProxyRequestRefused(ProxyRequestRefused::from_bytes(buffer)?),
b'9' => Self::BadRequest(BadRequest::from_bytes(buffer)?),
b => bail!("Unexpected second byte: {b}"),
}),
None => bail!("Invalid request"),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::General(this) => this.into_bytes(),
Self::NotFound(this) => this.into_bytes(),
Self::Gone(this) => this.into_bytes(),
Self::ProxyRequestRefused(this) => this.into_bytes(),
Self::BadRequest(this) => this.into_bytes(),
}
}
}
#[test]
fn test() {
// 50
{
let source = format!("50 message\r\n");
let target = Permanent::from_bytes(source.as_bytes()).unwrap();
match target {
Permanent::General(ref this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 51
{
let source = format!("51 message\r\n");
let target = Permanent::from_bytes(source.as_bytes()).unwrap();
match target {
Permanent::NotFound(ref this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 52
{
let source = format!("52 message\r\n");
let target = Permanent::from_bytes(source.as_bytes()).unwrap();
match target {
Permanent::Gone(ref this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 53
{
let source = format!("53 message\r\n");
let target = Permanent::from_bytes(source.as_bytes()).unwrap();
match target {
Permanent::ProxyRequestRefused(ref this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 59
{
let source = format!("59 message\r\n");
let target = Permanent::from_bytes(source.as_bytes()).unwrap();
match target {
Permanent::BadRequest(ref this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"59";
/// [Bad request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request)
pub struct BadRequest {
pub message: Option<String>,
}
impl BadRequest {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("59 message\r\n");
let source = BadRequest::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"50";
/// [General permanent failure code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50)
pub struct General {
pub message: Option<String>,
}
impl General {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("50 message\r\n");
let source = General::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"52";
/// [Gone](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone)
pub struct Gone {
pub message: Option<String>,
}
impl Gone {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("52 message\r\n");
let source = Gone::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"51";
/// [Not found](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found)
pub struct NotFound {
pub message: Option<String>,
}
impl NotFound {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("51 message\r\n");
let source = NotFound::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"53";
/// [Proxy request refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused)
pub struct ProxyRequestRefused {
pub message: Option<String>,
}
impl ProxyRequestRefused {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("53 message\r\n");
let source = ProxyRequestRefused::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,119 @@
pub mod cgi_error;
pub mod general;
pub mod proxy_error;
pub mod server_unavailable;
pub mod slow_down;
pub use cgi_error::CgiError;
pub use general::General;
pub use proxy_error::ProxyError;
pub use server_unavailable::ServerUnavailable;
pub use slow_down::SlowDown;
use anyhow::{bail, Result};
pub enum Temporary {
/// [General temporary failure](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40)
General(General),
/// [Server unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable)
ServerUnavailable(ServerUnavailable),
/// [CGI error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error)
CgiError(CgiError),
/// [Proxy error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error)
ProxyError(ProxyError),
/// [Slow down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down)
SlowDown(SlowDown),
}
impl Temporary {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
if buffer.first().is_none_or(|b| *b != b'4') {
bail!("Unexpected first byte")
}
match buffer.get(1) {
Some(byte) => Ok(match byte {
b'0' => Self::General(General::from_bytes(buffer)?),
b'1' => Self::ServerUnavailable(ServerUnavailable::from_bytes(buffer)?),
b'2' => Self::CgiError(CgiError::from_bytes(buffer)?),
b'3' => Self::ProxyError(ProxyError::from_bytes(buffer)?),
b'4' => Self::SlowDown(SlowDown::from_bytes(buffer)?),
b => bail!("Unexpected second byte: {b}"),
}),
None => bail!("Invalid request"),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::General(this) => this.into_bytes(),
Self::ServerUnavailable(this) => this.into_bytes(),
Self::CgiError(this) => this.into_bytes(),
Self::ProxyError(this) => this.into_bytes(),
Self::SlowDown(this) => this.into_bytes(),
}
}
}
#[test]
fn test() {
// 40
{
let source = format!("40 message\r\n");
let target = Temporary::from_bytes(source.as_bytes()).unwrap();
match target {
Temporary::General(ref this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 41
{
let source = format!("41 message\r\n");
let target = Temporary::from_bytes(source.as_bytes()).unwrap();
match target {
Temporary::ServerUnavailable(ref this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 42
{
let source = format!("42 message\r\n");
let target = Temporary::from_bytes(source.as_bytes()).unwrap();
match target {
Temporary::CgiError(ref this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 43
{
let source = format!("43 message\r\n");
let target = Temporary::from_bytes(source.as_bytes()).unwrap();
match target {
Temporary::ProxyError(ref this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
// 44
{
let source = format!("44 message\r\n");
let target = Temporary::from_bytes(source.as_bytes()).unwrap();
match target {
Temporary::SlowDown(ref this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
}
assert_eq!(target.into_bytes(), source.as_bytes());
}
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"42";
/// [CGI error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error)
pub struct CgiError {
pub message: Option<String>,
}
impl CgiError {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("42 message\r\n");
let source = CgiError::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"40";
/// [General temporary failure code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40)
pub struct General {
pub message: Option<String>,
}
impl General {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("40 message\r\n");
let source = General::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"43";
/// [Proxy error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error)
pub struct ProxyError {
pub message: Option<String>,
}
impl ProxyError {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("43 message\r\n");
let source = ProxyError::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"41";
/// [Server unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable)
pub struct ServerUnavailable {
pub message: Option<String>,
}
impl ServerUnavailable {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("41 message\r\n");
let source = ServerUnavailable::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"44";
/// [Slow down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down)
pub struct SlowDown {
pub message: Option<String>,
}
impl SlowDown {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("44 message\r\n");
let source = SlowDown::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

60
src/response/input.rs Normal file
View file

@ -0,0 +1,60 @@
pub mod default;
pub mod sensitive;
pub use default::Default;
pub use sensitive::Sensitive;
use anyhow::{bail, Result};
/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected)
pub enum Input {
Default(Default),
Sensitive(Sensitive),
}
impl Input {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
if buffer.first().is_none_or(|b| *b != b'1') {
bail!("Unexpected first byte")
}
match buffer.get(1) {
Some(byte) => Ok(match byte {
b'0' => Self::Default(Default::from_bytes(buffer)?),
b'1' => Self::Sensitive(Sensitive::from_bytes(buffer)?),
b => bail!("Unexpected second byte: {b}"),
}),
None => bail!("Invalid request"),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::Default(this) => this.into_bytes(),
Self::Sensitive(this) => this.into_bytes(),
}
}
}
#[test]
fn test() {
// 10
let request = format!("10 message\r\n");
let source = Input::from_bytes(request.as_bytes()).unwrap();
match source {
Input::Default(ref this) => assert_eq!(this.message, Some("message".to_string())),
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
// 11
let request = format!("11 message\r\n");
let source = Input::from_bytes(request.as_bytes()).unwrap();
match source {
Input::Sensitive(ref this) => {
assert_eq!(this.message, Some("message".to_string()))
}
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"10";
/// [Default input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10)
pub struct Default {
pub message: Option<String>,
}
impl Default {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("10 message\r\n");
let source = Default::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,70 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"11";
/// [Sensitive input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input)
pub struct Sensitive {
pub message: Option<String>,
}
impl Sensitive {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
message: String::from_utf8(m).map(|m| if m.is_empty() { None } else { Some(m) })?,
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
match self.message {
Some(message) => {
let mut bytes = Vec::with_capacity(message.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(message.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
None => {
let mut bytes = Vec::with_capacity(4);
bytes.extend(CODE);
bytes.extend([b'\r', b'\n']);
bytes
}
}
}
}
#[test]
fn test() {
let request = format!("11 message\r\n");
let source = Sensitive::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.message, Some("message".to_string()));
assert_eq!(source.into_bytes(), request.as_bytes());
}

60
src/response/redirect.rs Normal file
View file

@ -0,0 +1,60 @@
pub mod permanent;
pub mod temporary;
pub use permanent::Permanent;
pub use temporary::Temporary;
use anyhow::{bail, Result};
/// [Redirect](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection)
pub enum Redirect {
Permanent(Permanent),
Temporary(Temporary),
}
impl Redirect {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
if buffer.first().is_none_or(|b| *b != b'3') {
bail!("Unexpected first byte")
}
match buffer.get(1) {
Some(byte) => Ok(match byte {
b'0' => Self::Temporary(Temporary::from_bytes(buffer)?),
b'1' => Self::Permanent(Permanent::from_bytes(buffer)?),
b => bail!("Unexpected second byte: {b}"),
}),
None => bail!("Invalid request"),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::Permanent(this) => this.into_bytes(),
Self::Temporary(this) => this.into_bytes(),
}
}
}
#[test]
fn test() {
// 30
let request = format!("30 message\r\n");
let source = Redirect::from_bytes(request.as_bytes()).unwrap();
match source {
Redirect::Temporary(ref this) => {
assert_eq!(this.target, "message".to_string())
}
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
// 31
let request = format!("31 message\r\n");
let source = Redirect::from_bytes(request.as_bytes()).unwrap();
match source {
Redirect::Permanent(ref this) => assert_eq!(this.target, "message".to_string()),
_ => panic!(),
}
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,65 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"31";
/// [Permanent redirect](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection)
pub struct Permanent {
pub target: String,
}
impl Permanent {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut t = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
t.push(*b)
}
Ok(Self {
target: if t.is_empty() {
bail!("Target required")
} else {
String::from_utf8(t)?
},
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.target.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(self.target.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
}
#[test]
fn test() {
let request = format!("31 target\r\n");
let source = Permanent::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.target, "target".to_string());
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,65 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"30";
/// [Temporary redirect](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection)
pub struct Temporary {
pub target: String,
}
impl Temporary {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut t = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
t.push(*b)
}
Ok(Self {
target: if t.is_empty() {
bail!("Target required")
} else {
String::from_utf8(t)?
},
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.target.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(self.target.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
}
#[test]
fn test() {
let request = format!("30 target\r\n");
let source = Temporary::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.target, "target".to_string());
assert_eq!(source.into_bytes(), request.as_bytes());
}

42
src/response/success.rs Normal file
View file

@ -0,0 +1,42 @@
pub mod default;
pub use default::Default;
use anyhow::{bail, Result};
/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success)
pub enum Success {
Default(Default),
}
impl Success {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
if buffer.first().is_none_or(|b| *b != b'2') {
bail!("Unexpected first byte")
}
match buffer.get(1) {
Some(byte) => Ok(match byte {
b'0' => Self::Default(Default::from_bytes(buffer)?),
b => bail!("Unexpected second byte: {b}"),
}),
None => bail!("Invalid request"),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::Default(this) => this.into_bytes(),
}
}
}
#[test]
fn test() {
let request = format!("20 text/gemini\r\n");
let source = Success::from_bytes(request.as_bytes()).unwrap();
match source {
Success::Default(ref this) => {
assert_eq!(this.mime, "text/gemini".to_string())
}
}
assert_eq!(source.into_bytes(), request.as_bytes());
}

View file

@ -0,0 +1,65 @@
use anyhow::{bail, Result};
pub const CODE: &[u8] = b"20";
/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success)
pub struct Default {
pub mime: String,
}
impl Default {
/// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
// calculate length once
let len = buffer.len();
// validate headers for this response type
if !(3..=1024).contains(&len) {
bail!("Unexpected header length")
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{
bail!("Invalid status code")
}
// collect data bytes
let mut m = Vec::with_capacity(len);
for b in buffer[3..].iter() {
if *b == b'\r' {
continue;
}
if *b == b'\n' {
break;
}
m.push(*b)
}
Ok(Self {
mime: if m.is_empty() {
bail!("Content type required")
} else {
String::from_utf8(m)?
},
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.mime.len() + 5);
bytes.extend(CODE);
bytes.push(b' ');
bytes.extend(self.mime.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes
}
}
#[test]
fn test() {
let request = format!("20 text/gemini\r\n");
let source = Default::from_bytes(request.as_bytes()).unwrap();
assert_eq!(source.mime, "text/gemini".to_string());
assert_eq!(source.into_bytes(), request.as_bytes());
}