mirror of
https://github.com/YGGverse/titanite.git
synced 2026-03-31 09:05:31 +00:00
implement Response api
This commit is contained in:
parent
20284a7726
commit
07aae35a0e
28 changed files with 2292 additions and 1 deletions
|
|
@ -1,2 +1,5 @@
|
|||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use request::Request;
|
||||
pub use response::Response;
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ impl<'a> Titan<'a> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_bytes() {
|
||||
fn test() {
|
||||
const DATA: &[u8] = &[1, 2, 3];
|
||||
const MIME: &str = "plain/text";
|
||||
const TOKEN: &str = "token";
|
||||
|
|
|
|||
366
src/response.rs
Normal file
366
src/response.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
75
src/response/certificate.rs
Normal file
75
src/response/certificate.rs
Normal 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());
|
||||
}
|
||||
70
src/response/certificate/expected.rs
Normal file
70
src/response/certificate/expected.rs
Normal 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());
|
||||
}
|
||||
70
src/response/certificate/not_authorized.rs
Normal file
70
src/response/certificate/not_authorized.rs
Normal 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());
|
||||
}
|
||||
70
src/response/certificate/not_valid.rs
Normal file
70
src/response/certificate/not_valid.rs
Normal 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
203
src/response/failure.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/response/failure/permanent.rs
Normal file
118
src/response/failure/permanent.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
70
src/response/failure/permanent/bad_request.rs
Normal file
70
src/response/failure/permanent/bad_request.rs
Normal 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());
|
||||
}
|
||||
70
src/response/failure/permanent/general.rs
Normal file
70
src/response/failure/permanent/general.rs
Normal 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());
|
||||
}
|
||||
70
src/response/failure/permanent/gone.rs
Normal file
70
src/response/failure/permanent/gone.rs
Normal 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());
|
||||
}
|
||||
70
src/response/failure/permanent/not_found.rs
Normal file
70
src/response/failure/permanent/not_found.rs
Normal 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());
|
||||
}
|
||||
70
src/response/failure/permanent/proxy_request_refused.rs
Normal file
70
src/response/failure/permanent/proxy_request_refused.rs
Normal 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());
|
||||
}
|
||||
119
src/response/failure/temporary.rs
Normal file
119
src/response/failure/temporary.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
70
src/response/failure/temporary/cgi_error.rs
Normal file
70
src/response/failure/temporary/cgi_error.rs
Normal 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());
|
||||
}
|
||||
70
src/response/failure/temporary/general.rs
Normal file
70
src/response/failure/temporary/general.rs
Normal 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());
|
||||
}
|
||||
70
src/response/failure/temporary/proxy_error.rs
Normal file
70
src/response/failure/temporary/proxy_error.rs
Normal 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());
|
||||
}
|
||||
70
src/response/failure/temporary/server_unavailable.rs
Normal file
70
src/response/failure/temporary/server_unavailable.rs
Normal 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());
|
||||
}
|
||||
70
src/response/failure/temporary/slow_down.rs
Normal file
70
src/response/failure/temporary/slow_down.rs
Normal 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
60
src/response/input.rs
Normal 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());
|
||||
}
|
||||
70
src/response/input/default.rs
Normal file
70
src/response/input/default.rs
Normal 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());
|
||||
}
|
||||
70
src/response/input/sensitive.rs
Normal file
70
src/response/input/sensitive.rs
Normal 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
60
src/response/redirect.rs
Normal 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());
|
||||
}
|
||||
65
src/response/redirect/permanent.rs
Normal file
65
src/response/redirect/permanent.rs
Normal 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());
|
||||
}
|
||||
65
src/response/redirect/temporary.rs
Normal file
65
src/response/redirect/temporary.rs
Normal 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
42
src/response/success.rs
Normal 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());
|
||||
}
|
||||
65
src/response/success/default.rs
Normal file
65
src/response/success/default.rs
Normal 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());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue