trotter 1.0.2

Trotter 🎠 is an experimental crate that aims to make writing Gemini clients fun and easy.
Documentation
use crate::error::ResponseErr;
use openssl::{hash::MessageDigest, x509::X509};
use std::{fs::File, io::Write, path::PathBuf};

/// A gemini response.
#[derive(Debug)]
pub struct Response {
	pub status: u8,
	pub meta: String,
	pub content: Vec<u8>,
	/// The certificate of the responding server.
	pub certificate: X509,
}

type Result<T> = std::result::Result<T, ResponseErr>;

impl Response {
	/// Returns true if the response is gemtext.
	pub fn is_gemtext(&self) -> bool {
		if let Some(pos) = self.meta.find("text/gemini") {
			if pos == 0 {
				return true;
			}
		}
		false
	}

	/// Return gemtext (if any) inside this response.
	pub fn gemtext(&self) -> Result<String> {
		self.require_status(20)?;

		if self.is_gemtext() {
			return self.text();
		}

		Err(ResponseErr::UnexpectedFiletype(
			"text/gemini".to_string(),
			self.meta.clone(),
		))
	}

	/// Return utf8 text (if any) inside this response, regardless of mimetype.
	pub fn text(&self) -> Result<String> {
		self.require_status(20)?;
		Ok(std::str::from_utf8(&self.content)
			.map_err(ResponseErr::Utf8Content)?
			.to_string())
	}

	/// Save response to file.
	pub fn save(&self, file: &mut File) -> Result<()> {
		self.require_status(20)?;
		file.write_all(&self.content)
			.map_err(ResponseErr::FileWrite)?;
		Ok(())
	}

	/// Save response to path.
	pub fn save_to_path(&self, path: impl Into<PathBuf>) -> Result<()> {
		self.require_status(20)?;

		let path = path.into();
		let mut file = File::create(path).map_err(ResponseErr::FileCreate)?;
		file.write_all(&self.content)
			.map_err(ResponseErr::FileWrite)?;
		Ok(())
	}

	/// Return the server's certificate pem
	pub fn certificate_pem(&self) -> Result<String> {
		Ok(std::str::from_utf8(
			&self
				.certificate
				.to_pem()
				.map_err(ResponseErr::SerializingToPem)?,
		)
		.map_err(ResponseErr::PemInvalidUtf8)?
		.to_string())
	}

	/// Returns a human-readable string displaying info about the
	/// server's certificate.
	///
	/// *primarily for debugging purposes*
	pub fn certificate_info(&self) -> Result<String> {
		Ok(std::str::from_utf8(
			&self
				.certificate
				.to_text()
				.map_err(ResponseErr::FailedToInspectCert)?,
		)
		.map_err(ResponseErr::CertInfoIsntValidUtf8)?
		.to_string())
	}

	/// Return the server certificate's fingerprint
	pub fn certificate_fingerprint(&self) -> Result<String> {
		use std::fmt::Write;
		Ok(self
			.certificate
			.digest(MessageDigest::sha256())
			.map_err(ResponseErr::FailedToFingerprint)?
			.iter()
			.fold(String::new(), |mut out, x| {
				let _ = write!(out, "{:02x}", x);
				out
			}))
	}

	/// (private) Error if `s` doesn't match the status
	fn require_status(&self, s: u8) -> Result<()> {
		if self.status != s {
			Err(ResponseErr::UnexpectedStatus {
				expected: s.into(),
				received: self.status.into(),
				meta: self.meta.clone(),
			})
		} else {
			Ok(())
		}
	}
}