kochab/src/types/body.rs

126 lines
3.7 KiB
Rust

use tokio::io::AsyncRead;
#[cfg(feature="scgi_srv")]
use tokio::io::AsyncReadExt;
#[cfg(feature="serve_dir")]
use tokio::fs::File;
#[cfg(feature = "gemtext")]
use crate::Gemtext;
/// The body of a response
///
/// The content of a successful response to be sent back to the user. This can be either
/// some bytes which will be sent directly to the user, or a reader which will be read at
/// some point before sending to the user.
pub enum Body {
/// In-memory bytes that may be sent back to the user
Bytes(Vec<u8>),
/// A reader which will be streamed to the user
///
/// If a reader blocks for too long, it MAY be killed before finishing, which results
/// in the user receiving a malformed response or timing out.
Reader(Box<dyn AsyncRead + Send + Sync + Unpin>),
}
impl Body {
/// Called by [`Response::rewrite_all`]
#[cfg(feature="scgi_srv")]
pub (crate) async fn rewrite_all(&mut self, based_on: &crate::Request) -> std::io::Result<bool> {
let bytes = match self {
Self::Bytes(bytes) => {
let mut newbytes = Vec::new();
std::mem::swap(bytes, &mut newbytes);
newbytes
}
Self::Reader(reader) => {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
bytes
}
};
let mut body = String::from_utf8(bytes).expect("text/gemini wasn't UTF8");
let mut maybe_indx = if body.starts_with("=> ") {
Some(3)
} else {
body.find("\n=> ").map(|offset| offset + 4)
};
while let Some(indx) = maybe_indx {
// Find the end of the link part
let end = (&body[indx..]).find(&[' ', '\n', '\r'][..])
.map(|offset| indx + offset )
.unwrap_or(body.len());
// Perform replacement
if let Some(replacement) = based_on.rewrite_path(&body[indx..end]) {
body.replace_range(indx..end, replacement.as_str());
} else {
return Ok(false)
};
// Find next match
maybe_indx = (&body[indx..]).find("\n=> ").map(|offset| offset + 4 + indx);
}
*self = Self::Bytes(body.into_bytes());
Ok(true)
}
}
#[cfg(feature = "gemtext")]
#[allow(clippy::fallible_impl_from)] // It's really not fallible but thanks
impl From<Vec<gemtext::Node>> for Body {
/// Render a series of [`gemtext`] nodes to a `text/gemini` body without [normalizing]
///
/// [normalizing]: Gemtext::normalize
fn from(document: Vec<gemtext::Node>) -> Self {
let size: usize = document.iter().map(gemtext::Node::estimate_len).sum();
let mut bytes = Vec::with_capacity(size + document.len());
gemtext::render(document, &mut bytes).unwrap(); // Safe: we're only writing to a buffer
Self::Bytes(bytes)
}
}
#[cfg(feature = "gemtext")]
impl From<Gemtext> for Body {
/// [Normalize][1] & eender a series of [`gemtext`] nodes to a `text/gemini` body
///
/// [1]: Gemtext::normalize
fn from(document: Gemtext) -> Self {
document.normalize().build().into()
}
}
impl From<Vec<u8>> for Body {
fn from(bytes: Vec<u8>) -> Self {
Self::Bytes(bytes)
}
}
impl<'a> From<&'a [u8]> for Body {
fn from(bytes: &[u8]) -> Self {
Self::Bytes(bytes.to_owned())
}
}
impl From<String> for Body {
fn from(text: String) -> Self {
Self::Bytes(text.into_bytes())
}
}
impl<'a> From<&'a str> for Body {
fn from(text: &str) -> Self {
Self::Bytes(text.to_owned().into_bytes())
}
}
#[cfg(feature="serve_dir")]
impl From<File> for Body {
fn from(file: File) -> Self {
Self::Reader(Box::new(file))
}
}