use crate::StatusCode; use num::FromPrimitive; use std::io::{prelude::*, ErrorKind, self}; /// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html). #[derive(Default)] pub struct Response { pub status: StatusCode, pub meta: String, pub body: Vec, } /// The parser state. #[derive(Debug)] enum State { ReadStatusCode { data: Vec }, ReadWhitespace, ReadMeta { data: Vec }, ReadBody { data: Vec }, } /// Response error. #[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, #[error("Response meta is too long")] ResponseMetaTooLong, } 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); } panic!("got here: {}, {:?}", n, state); } } Err(why) => { if why.kind() == ErrorKind::ConnectionAborted { if let State::ReadBody { data } = state { result.body = data; return Ok(result); } } return Err(Error::IO(why)); } } 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![] }; } _ => { if data.len() == 1024 { return Err(Error::ResponseMetaTooLong) } data.push(buf[0]); } }, State::ReadBody { data } => data.push(buf[0]), } } } pub fn write(self, out: &mut impl Write) -> io::Result<()> { write!(out, "{} {}\r\n", self.status.num(), self.meta)?; out.write(&self.body)?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::*; #[test] fn success() -> Result<(), Error> { let _ = pretty_env_logger::try_init(); let mut fin = std::fs::File::open("./testdata/simple_response.txt")?; let resp = Response::parse(&mut fin)?; assert_eq!(resp.meta, "text/gemini".to_string()); assert_eq!(resp.status, StatusCode::Success); Ok(()) } #[test] fn error() -> Result<(), Error> { let _ = pretty_env_logger::try_init(); let mut fin = std::fs::File::open("./testdata/error_response.txt")?; let resp = Response::parse(&mut fin)?; assert_eq!(resp.status, StatusCode::PermanentFailure); Ok(()) } #[test] fn not_found() -> Result<(), Error> { let _ = pretty_env_logger::try_init(); let mut fin = std::fs::File::open("./testdata/notfound_response.txt")?; let resp = Response::parse(&mut fin)?; assert_eq!(resp.status, StatusCode::NotFound); Ok(()) } #[test] fn meta_too_long() { let _ = pretty_env_logger::try_init(); let mut fin = std::fs::File::open("./testdata/meta_too_long.txt").unwrap(); match Response::parse(&mut fin) { Ok(_) => panic!("wanted error but didn't get one"), Err(why) => if let ResponseError::ResponseMetaTooLong = why { println!("ok"); } else { panic!("wanted ResponseError::ResponseMetaTooLong") }, } } }