use url::Url;
use crate::plain::exchange;
use crate::{Error, Response, Scheme, Status};
pub(crate) async fn fetch(url: &Url) -> Result<Response, Error> {
let host = url
.host_str()
.ok_or_else(|| Error::BadUrl("spartan URL has no host".into()))?;
let port = url.port().unwrap_or_else(|| Scheme::Spartan.default_port());
let path = if url.path().is_empty() { "/" } else { url.path() };
let request = format!("{host} {path} 0\r\n");
let raw = exchange(host, port, request.as_bytes()).await?;
parse(url, &raw)
}
fn parse(url: &Url, raw: &[u8]) -> Result<Response, Error> {
let split = raw
.windows(2)
.position(|w| w == b"\r\n")
.ok_or_else(|| Error::Protocol("response header has no CRLF".into()))?;
let header = std::str::from_utf8(&raw[..split])
.map_err(|_| Error::Protocol("response header is not UTF-8".into()))?;
let body = raw[split + 2..].to_vec();
let mut parts = header.splitn(2, ' ');
let code_str = parts.next().unwrap_or("");
let meta = parts.next().unwrap_or("").trim().to_string();
let code: u8 = code_str
.parse()
.map_err(|_| Error::Protocol(format!("bad spartan status: {header:?}")))?;
let status = match code {
2 => Status::Success,
3 => Status::Redirect,
4 | 5 => Status::Failure,
_ => return Err(Error::Protocol(format!("unknown spartan status: {code}"))),
};
Ok(Response {
url: url.clone(),
status,
raw_status: Some(code),
meta,
body: if status == Status::Success { body } else { Vec::new() },
})
}
#[cfg(test)]
mod tests {
use super::*;
fn u() -> Url {
Url::parse("spartan://example.org/").unwrap()
}
#[test]
fn success_carries_mime_and_body() {
let r = parse(&u(), b"2 text/gemini\r\n# Hi\n").unwrap();
assert_eq!(r.status, Status::Success);
assert_eq!(r.raw_status, Some(2));
assert_eq!(r.mime(), Some("text/gemini"));
assert_eq!(r.body, b"# Hi\n");
}
#[test]
fn redirect_drops_body() {
let r = parse(&u(), b"3 /elsewhere\r\nx").unwrap();
assert_eq!(r.status, Status::Redirect);
assert_eq!(r.meta, "/elsewhere");
assert!(r.body.is_empty());
}
#[test]
fn client_and_server_errors_are_failures() {
assert_eq!(parse(&u(), b"4 bad request\r\n").unwrap().status, Status::Failure);
assert_eq!(parse(&u(), b"5 boom\r\n").unwrap().status, Status::Failure);
}
#[test]
fn missing_crlf_is_a_protocol_error() {
assert!(matches!(parse(&u(), b"2 text/gemini"), Err(Error::Protocol(_))));
}
}