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), /// 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), } 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 { 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> for Body { /// Render a series of [`gemtext`] nodes to a `text/gemini` body without [normalizing] /// /// [normalizing]: Gemtext::normalize fn from(document: Vec) -> 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 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> for Body { fn from(bytes: Vec) -> Self { Self::Bytes(bytes) } } impl<'a> From<&'a [u8]> for Body { fn from(bytes: &[u8]) -> Self { Self::Bytes(bytes.to_owned()) } } impl From 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 for Body { fn from(file: File) -> Self { Self::Reader(Box::new(file)) } }