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, } /// The parser state. #[derive(Debug)] enum State { ReadStatusCode { data: Vec }, ReadWhitespace, ReadMeta { data: Vec }, ReadBody { data: Vec }, } #[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 { 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(()) } }