maj/src/response.rs

117 lines
3.4 KiB
Rust

use crate::StatusCode;
use num::FromPrimitive;
use std::io::prelude::*;
/// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html).
#[derive(Default)]
pub struct Response {
status: StatusCode,
meta: String,
body: Vec<u8>,
}
/// The parser state.
#[derive(Debug)]
enum State {
ReadStatusCode { data: Vec<u8> },
ReadWhitespace,
ReadMeta { data: Vec<u8> },
ReadBody { data: Vec<u8> },
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("unexpected end of file found while parsing response")]
EOF,
#[error("I/O error")]
IO(#[from] std::io::Error),
#[error("invalid status code character {0}")]
InvalidStatusCode(u8),
#[error("UTF-8 error: {0}")]
Utf8(#[from] std::str::Utf8Error),
#[error("Number parsing error: {0}")]
NumParse(#[from] std::num::ParseIntError),
#[error("None found when none should not be found")]
NoneFound,
}
impl Response {
pub fn parse(inp: &mut impl Read) -> Result<Response, Error> {
let mut state = State::ReadStatusCode { data: vec![] };
let mut buf = [0; 1];
let mut result = Response::default();
loop {
match inp.read(&mut buf) {
Ok(n) => {
if n == 0 {
if let State::ReadBody { data } = state {
result.body = data;
return Ok(result);
}
}
log::trace!("buf: {:?}: {:?}", buf, buf[0] as char);
}
Err(why) => return Err(Error::IO(why)),
}
log::trace!("state: {:?}", state);
match &mut state {
State::ReadStatusCode { data } => match buf[0] as char {
'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0' => {
data.push(buf[0]);
}
' ' | '\t' => {
let status_code: &str = std::str::from_utf8(data)?;
let status_code: u8 = status_code.parse()?;
result.status = StatusCode::from_u8(status_code).ok_or(Error::NoneFound)?;
state = State::ReadWhitespace;
}
foo => return Err(Error::InvalidStatusCode(foo as u8)),
},
State::ReadWhitespace => match buf[0] as char {
' ' | '\t' => {}
_ => {
state = State::ReadMeta { data: vec![buf[0]] };
}
},
State::ReadMeta { data } => match buf[0] as char {
'\r' => {}
'\n' => {
result.meta = std::str::from_utf8(data)?.to_string();
state = State::ReadBody { data: vec![] };
}
_ => data.push(buf[0]),
},
State::ReadBody { data } => data.push(buf[0]),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
use std::io::prelude::*;
#[test]
fn parse() -> Result<(), Error> {
pretty_env_logger::init();
let mut fin = std::fs::File::open("./testdata/simple_response.txt")?;
Response::parse(&mut fin)?;
Ok(())
}
}