FZIABKEV74PFFRFETMT5WVL625I76UYN2LXKIKRZNQE5GDGX3GUQC ARXVO5XAQFZR3X6MXW525Z6WXUYUHRJUGTTNCEKKXRAKQQBG5ZXAC JWLMAS7UWFXHWBYEBW4YSLPZUF7UFQHAKN3AUF2IAJWVXW46T5PQC 3SPNKI46URBW4PD3JRMBV5YV5W56N3SFUTK25VQEF2FUZ64ZDQPAC WGRFJRTEXOKY7PV536O62WUE32T24VQEHFQP67GEZZ67AE37ZWUAC AOJXTWBZA2DQBFTJZX2YTGAKIB62SKTEVJ7RRIBJNDSMC75DDQVQC DBKKKHC2ER7XAYR6HAFQ6I75M3OLDOSGCB5BTEP7X6XAZRSWKIRAC HVFBGX2A2LBL6HSFKPNIG5KGQPVMZ5GFYJ4ZPDAM4POSRTQSKN7QC AMTMTTJTF2XHEUC6ACP2PHP7GAFMFZRSJ47VJBKEO22X4ZGFIX7QC OSV63NXNIRGOEJZUECHXYYYEFUAFXLN4BNIH3MKLE7SDZLHGFEWAC ONQEIR5BV26QJSI3HY7B3MN6VWEGSZIXAIUIANJ5ZLR3QN3DFGEAC use response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success};use server::{Network, TcpNetwork};use std::io::{BufReader, Read, Result as ioResult, Write};
use server::Network;use std::{fmt::Display,io::{BufReader, Read, Result as ioResult, Write},};
pub enum Error {}impl App {pub fn run(self) -> Result<(), Error> {let mut cli = Cli {};if self.test_prompt {let overt = cli.read("tell me something");cli.warn(format_args!("your response was: {overt}"));let secret = cli.read_secret("tell me your secrets");cli.warn(format_args!("your secret was: {secret}"));Ok(())} else {let url = self.url.unwrap_or_else(|| {"gemini://gemini.circumlunar.space/docs/specification.gmi".into()});let url: Url = url.parse().expect("Not a valid URL");run::<TcpNetwork<danger::Naive>>(&mut cli, url);Ok(())}}}pub struct Cli {}impl UI for Cli {fn show(&self, content: Media) {match content {Media::Gemini(g) => println!("{g:#?}"), // TODOMedia::Text(s) => println!("{s}"),}}fn read(&self, prompt: impl Display) -> String {print!("{prompt}: ");stdout().flush().unwrap();std::io::stdin().lines().next().expect("End of input").expect("Failed to read input")}fn read_secret(&self, prompt: impl Display) -> String {let dim = ansi_term::Style::new().dimmed();print!("{prompt}: ");let hidden = "(input hidden)";let len = hidden.len() as u32;let back = ansi_control_codes::control_sequences::CUB(Some(len));print!("{}{back}", dim.paint(hidden));stdout().flush().unwrap();rpassword::read_password().expect("Failed to read input")}fn warn<D: std::fmt::Display>(&self, warning: D) {println!("Warning: {warning}")}}}pub trait UI {fn show(&self, content: media::Media);fn warn<D: Display>(&self, warning: D);fn read(&self, prompt: impl Display) -> String;fn read_secret(&self, prompt: impl Display) -> String;
let url = cli.url.unwrap_or_else(|| "gemini://gemini.circumlunar.space/docs/specification.gmi".into());let mut url: Url = url.parse().expect("Not a valid URL");while let Some(new_url) = run_url::<TcpNetwork<danger::Naive>>(url) {
match cli.run() {Ok(()) => println!("done"),Err(e) => match e {},}}fn run<N: Network>(ui: &mut impl UI, mut url: Url)where<N as Network>::Error: std::fmt::Debug,{while let Some(new_url) = run_url::<N>(ui, url) {
response::Header::Input(s) => {let Input { prompt, sensitive } = s;println!("{prompt}");let input = if let Some(true) = sensitive {// Read sensitive inputrpassword::read_password()} else {// Read plain inputstd::io::stdin().lines().next().expect("End of input")}.expect("Failed to read input");let input = urlencoding::encode(&input);url.set_query(Some(&input));Some(url)}
response::Header::Input(s) => handle::input(s, ui, url),
let Success { mime } = s;let body = stream.read_string().expect("failed to read body");//TODO remove this debug outputprintln!("page is encoded as {mime:?}");println!("response body: {body}");let media = decode_media(mime, &body);media.display();
handle::success(s, stream, ui);
}response::Header::Redirect(s) => {let Redirect { url, temporary } = s;if let Some(false) = temporary {println!("Server has permanently moved. Redirecting to: {url}");println!("Please update your records!");} else {println!("Server has temporarily moved. Redirecting to: {url}");}Some(url)
}}}mod handle {use crate::{decode_media,io::ReadExt,response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success},UI,};use std::io::Read;use url::Url;pub fn cert_required(s: CertRequired, ui: &mut impl UI) {let CertRequired { message, typ } = s;ui.warn(format_args!("Certificate required: {typ:?}! Details:\n{message}"));}pub fn fail_perm(s: FailPerm, ui: &mut impl UI) {let FailPerm { message, typ } = s;ui.warn(format_args!("Permanent failure: {typ:?}! Details:\n{message}"));}pub fn fail_temp(s: FailTemp, ui: &mut impl UI) {let FailTemp { message, typ } = s;ui.warn(format_args!("Temporary failure: {typ:?}! Please try again. Details:\n{message}"));}pub fn redirect(s: Redirect, ui: &mut impl UI) -> Url {let Redirect { url, temporary } = s;if let Some(false) = temporary {ui.warn(format_args!("Server has permanently moved. Redirecting to: {url}\nPlease update your records!"));} else {ui.warn(format_args!("Server has temporarily moved. Redirecting to: {url}"));
url}pub fn success(s: Success, mut stream: impl Read, ui: &mut impl UI) {let Success { mime } = s;let body = stream.read_string().expect("failed to read body");//TODO remove this debug outputprintln!("page is encoded as {mime:?}");println!("response body: {body}");let media = decode_media(mime, &body);ui.show(media);}pub fn input(s: Input, ui: &mut impl UI, mut url: Url) -> Option<Url> {let Input { prompt, sensitive } = s;let input = if let Some(true) = sensitive {ui.read_secret(&prompt)} else {ui.read(&prompt)};let input = urlencoding::encode(&input);url.set_query(Some(&input));Some(url)
let media = match mime.type_() {mime::TEXT => match mime.subtype().as_ref() {"gemini" => Media::Gemini(body.into()),_ => Media::Text(body),},
let media = match (mime.type_(), mime.subtype().as_ref()) {(mime::TEXT, "gemini") => Media::Gemini(body.into()),
let url = "gopher://abc.de".parse().unwrap();let mut stream: &[u8] = b"";let Some(url) = handle_response_header(head, url, &mut stream)else {panic!("expected redirect handling to return a url")};
let mut app = Cli {};let url = {let crate::response::Header::Redirect(s) = headelse {panic!("expected redirect")};crate::handle::redirect(s, &mut app)};
}#[test]fn test_request() {let url: Url = "gemini://some/url".try_into().unwrap();let req = b"gemini://some/url\r\n";let mut net_stream = [0; 1024];send_request(&mut net_stream.as_mut_slice(), &url).expect("error sending request");assert_eq!(req, &net_stream[..req.len()]);}#[rstest]#[case('0', Some(false))]#[case('1', Some(true))]#[case('3', None)]fn test_response_input(#[case] sensitive_code: char, #[case] sensitive: Option<bool>) {let exp_head = Header::Input(header::Input {prompt: "prompt text".to_string(),sensitive,});let resp_text = format!("1{sensitive_code} prompt text\r\n");let resp_head: &[u8] = resp_text.as_bytes();let head = read_response_header(&mut &*resp_head).expect("failed to read response");assert_eq!(exp_head, head);
name = "ansi-control-codes"version = "0.0.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b7ca68a5142d31bc74840dc5520efedf65a3feb348fcca74d38ae1dadc0822f8"[[package]]name = "ansi_term"version = "0.12.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"dependencies = ["winapi",][[package]]