#![allow(clippy::new_without_default)] extern crate bunt; extern crate dirs; extern crate tempfile; extern crate simple_input; use std::env; use std::path::Path; use std::process::{exit, Command}; use std::thread::sleep; use std::time::Duration; use std::fs::{self, File}; use std::io::{Read, self}; use simple_input::input; use tempfile::NamedTempFile; mod config; use config::{Config}; mod util; use util::{usage, banner, dial, make_key, make_key_str, is_valid_directory_entry, view_telnet}; #[derive(Clone, Debug)] pub struct Link { pub next: Option>, pub which: Option, pub key: usize, pub host: String, pub port: u16, pub selector: String, } #[derive(Clone, Debug)] pub struct BrowserState { pub tmpfilename: String, pub links: Option>, pub history: Option>, pub link_key: usize, pub current_host: String, pub current_port: u16, pub current_selector: String, pub parsed_host: String, pub parsed_port: u16, pub parsed_selector: String, pub bookmarks: Vec, pub config: Config, } impl BrowserState { pub fn new() -> Self { Self { tmpfilename: String::new(), links: None, history: None, link_key: 0, current_host: String::new(), current_port: 0, current_selector: String::new(), parsed_host: String::new(), parsed_port: 0, parsed_selector: String::new(), bookmarks: Vec::new(), config: Config::new(), } } pub fn download_file( &self, host: &str, port: u16, selector: &str, file: &mut File, ) -> bool { if self.config.verbose { eprintln!("downloading [{}]...", selector); } let stream = dial(&host, port, selector); if stream.is_none() { eprintln!("error: downloading [{}] failed", selector); return false; } // todo show progress match io::copy(&mut stream.unwrap(), file) { Ok(b) => eprintln!("downloaded {} bytes", b), Err(e) => { eprintln!("error: failed to download file: {}", e); return false; } }; if self.config.verbose { eprintln!("downloading [{}] complete", selector); } true } pub fn download_temp( &mut self, host: &str, port: u16, selector: &str, ) -> bool { let mut tmpfile = match NamedTempFile::new() { Ok(f) => f, Err(_) => { eprintln!("error: unable to create tmp file"); return false; } }; self.tmpfilename = tmpfile.path().display().to_string(); if !self.download_file(host, port, selector, tmpfile.as_file_mut()) { fs::remove_file(&self.tmpfilename).expect("failed to delete temp file"); return false; } let _ = tmpfile.keep(); true } pub fn add_link( &mut self, which: char, name: String, host: String, port: u16, selector: String, ) { let mut a: char = '\0'; let mut b: char = '\0'; let mut c: char = '\0'; if host.is_empty() || port == 0 || selector.is_empty() { return; } let link = Link { which: Some(which as u8 as char), key: self.link_key, host, port, selector, next: if self.links.is_none() { None } else { self.links.clone() }, }; self.links = Some(Box::new(link)); make_key_str(self.link_key, &mut a, &mut b, &mut c); self.link_key += 1; bunt::println!("{[green]}{[green]}{[green]} {}", a, b, c, name); } pub fn clear_links(&mut self) { self.links = None; self.link_key = 0; } pub fn add_history(&mut self) { let link: Link = Link { which: None, key: 0, host: self.current_host.clone(), port: self.current_port, selector: self.current_selector.clone(), next: if self.history.is_none() { None } else { self.history.clone() }, }; self.history = Some(Box::new(link)); } pub fn handle_directory_line(&mut self, line: &str) { let fields = { line[1..].split('\t') .enumerate() .filter(|(i, x)| *i == 0 || !x.is_empty()) .map(|(_, x)| x) .collect::>() }; /* determine listing type */ match line.chars().next() { Some('i') | Some('3') => println!(" {}", fields[0]), Some('.') => println!("\0"), // some gopher servers use this Some(w @ '0') | Some(w @ '1') | Some(w @ '5') | Some(w @ '7') | Some(w @ '8') | Some(w @ '9') | Some(w @ 'g') | Some(w @ 'I') | Some(w @ 'p') | Some(w @ 'h') | Some(w @ 's') => { match fields.len() { 1 => self.add_link( w, fields[0].to_string(), self.current_host.clone(), self.current_port, fields[0].to_string(), ), 2 => self.add_link( w, fields[0].to_string(), self.current_host.clone(), self.current_port, fields[1].to_string(), ), 3 => self.add_link( w, fields[0].to_string(), fields[2].to_string(), self.current_port, fields[1].to_string(), ), x if x >= 4 => self.add_link( w, fields[0].to_string(), fields[2].to_string(), fields[3].parse().unwrap_or(70), // todo oof fields[1].to_string(), ), _ => (), } } Some(x) => eprintln!("miss [{}]: {}", x, fields[0]), None => (), } } pub fn view_directory( &mut self, host: &str, port: u16, selector: &str, make_current: bool, ) { let stream = dial(&host, port, selector); let mut buffer = String::new(); self.clear_links(); let mut stream = match stream { Some(s) => s, None => return, }; /* only adapt current prompt when successful */ /* make history entry */ if make_current { self.add_history(); } /* don't overwrite the current_* things... */ if host != self.current_host { self.current_host = host.to_string(); } /* quit if not successful */ if port != self.current_port { self.current_port = port; } if selector != self.current_selector { self.current_selector = selector.to_string(); } if let Err(e) = stream.read_to_string(&mut buffer) { eprintln!("failed to read response body: {}", e); } for line in buffer.lines() { if !is_valid_directory_entry(line.chars().next().unwrap_or('\0')) { println!( "invalid: [{}] {}", line.chars().next().unwrap_or('\0'), line.chars().skip(1).collect::() ); continue; } self.handle_directory_line(line); } } pub fn view_file( &mut self, cmd: &str, host: &str, port: u16, selector: &str, ) { if !self.download_temp(host, port, selector) { return; } if self.config.verbose { println!("h({}) p({}) s({})", host, port, selector); } if self.config.verbose { println!("executing: {} {}", cmd, self.tmpfilename); } /* execute */ match Command::new(cmd).arg(&self.tmpfilename).spawn() { Ok(mut c) => if let Err(e) = c.wait() { eprintln!("failed to wait for command to exit: {}", e); }, Err(e) => eprintln!("error: failed to run command: {}", e), } /* to wait for browsers etc. that return immediately */ sleep(Duration::from_secs(1)); fs::remove_file(&self.tmpfilename).expect("failed to delete temp file"); } pub fn view_download( &mut self, host: &str, port: u16, selector: &str, ) { let mut filename: String = Path::new(selector).file_name().unwrap_or_default().to_string_lossy().into(); let line: String = match input(&format!( "enter filename for download [{}]: ", filename )) .as_str() { "" => { println!("download aborted"); return; } x => x.into(), }; filename = line; // TODO something stinky going on here let mut file = match File::create(&filename) { Ok(f) => f, Err(e) => { println!("error: unable to create file [{}]: {}", filename, e,); return; } }; if !self.download_file(host, port, selector, &mut file) { println!("error: unable to download [{}]", selector); fs::remove_file(filename).expect("failed to delete file"); } } pub fn view_search( &mut self, host: &str, port: u16, selector: &str, ) { let search_selector: String = match input("enter search string: ").as_str() { "" => { println!("search aborted"); return; } s => format!("{}\t{}", selector, s), }; self.view_directory(host, port, &search_selector, true); } pub fn view_history(&mut self, key: Option) { let mut history_key: usize = 0; let mut a: char = '\0'; let mut b: char = '\0'; let mut c: char = '\0'; let mut link: Option>; if self.history.is_none() { println!("(empty history)"); return; } if key.is_none() { println!("(history)"); link = self.history.clone(); while let Some(l) = link { let fresh10 = history_key; history_key += 1; make_key_str(fresh10, &mut a, &mut b, &mut c); bunt::println!("{[green]}{[green]}{[green]} {}:{}/{}", a, b, c, (*l).host, (*l).port, (*l).selector,); link = (*l).next } } else if let Some(key) = key { /* traverse history list */ link = self.history.clone(); while let Some(l) = link { if history_key == key { self.view_directory( &(*l).host, (*l).port, &(*l).selector, false, ); return; } link = (*l).next; history_key += 1 } println!("history item not found"); }; } pub fn view_bookmarks(&mut self, key: Option) { let mut a: char = '\0'; let mut b: char = '\0'; let mut c: char = '\0'; if key.is_none() { println!("(bookmarks)"); for (i, bookmark) in self.bookmarks.iter().enumerate() { make_key_str(i, &mut a, &mut b, &mut c); println!("{}{}{} {}", a, b, c, bookmark); } } else if let Some(key) = key { for (i, bookmark) in self.bookmarks.clone().iter().enumerate() { if i == key { if self.parse_uri(bookmark) { self.view_directory( &self.parsed_host.clone(), self.parsed_port, &self.parsed_selector.clone(), false, ); } else { println!("invalid gopher URI: {}", bookmark,); } return; } } }; } pub fn pop_history(&mut self) { match &self.history.clone() { None => println!("(empty history)"), Some(h) => { /* reload page from history (and don't count as history) */ self.view_directory( &(*h).host, (*h).port, &(*h).selector, false, ); /* history is history... :) */ self.history = h.next.clone(); } } } pub fn follow_link(&mut self, key: usize) -> bool { let mut link: Option> = self.links.clone(); while let Some(ref l) = link { if (*l).key != key { link = (*l).next.clone() } else if let Some(w) = (*l).which { match w { '0' => { self.view_file( &self.config.cmd_text.clone(), &(*l).host, (*l).port, &(*l).selector, ); } '1' => { self.view_directory( &(*l).host, (*l).port, &(*l).selector, true, ); } '7' => { self.view_search(&(*l).host, (*l).port, &(*l).selector); } '5' | '9' => { self.view_download(&(*l).host, (*l).port, &(*l).selector); } '8' => { view_telnet(&(*l).host, (*l).port); } 'f' | 'I' | 'p' => { self.view_file( &self.config.cmd_image.clone(), &(*l).host, (*l).port, &(*l).selector, ); } 'h' => { self.view_file( &self.config.cmd_browser.clone(), &(*l).host, (*l).port, &(*l).selector, ); } 's' => { self.view_file( &self.config.cmd_player.clone(), &(*l).host, (*l).port, &(*l).selector, ); } _ => { println!("missing handler [{}]", w); } } return true; } } false } pub fn download_link(&mut self, key: usize) { let mut link: Option> = self.links.clone(); while let Some(l) = link { if (*l).key != key { link = (*l).next } else { self.view_download(&(*l).host, (*l).port, &(*l).selector); return; } } println!("link not found"); } /* function prototypes */ pub fn parse_uri(&mut self, uri: &str) -> bool { let mut tmp: &str = &uri[..]; /* strip gopher:// */ if uri.starts_with("gopher://") { tmp = &uri[9..]; } self.parsed_host = tmp.chars().take_while(|x| !(*x == ':' || *x == '/')).collect(); if self.parsed_host.is_empty() { return false; } if tmp.contains(':') { let port_string: String = tmp .chars() .skip_while(|x| *x != ':') .skip(1) .take_while(|x| *x != '/') .collect(); if port_string.is_empty() { self.parsed_port = 70; } else { self.parsed_port = match port_string.parse() { Ok(p) => p, Err(_) => { eprintln!("failed to parse port"); exit(1); } }; } } tmp = &tmp[tmp.find('/').unwrap_or(0)..]; /* parse selector (ignore slash and selector type) */ if tmp.is_empty() || tmp.find('/').is_none() { self.parsed_selector = "/".to_string(); } else { self.parsed_selector = tmp.into(); } true } pub fn init(&mut self, argv: Vec) -> i32 { let mut i: usize = 1; /* copy defaults */ self.config.init(); self.bookmarks = self.config.bookmarks.clone(); let mut uri: String = self.config.start_uri.clone(); /* parse command line */ while i < argv.len() { if argv[i].starts_with('-') { match argv[i].chars().nth(1) { Some('H') => { usage(); } Some('v') => { banner(true); exit(0); } _ => { usage(); } } } else { uri = argv[i].clone(); } i += 1 } /* parse uri */ if !self.parse_uri(&uri) { banner(false); eprintln!("invalid gopher URI: {}", argv[i],); exit(1); } /* main loop */ self.view_directory( &self.parsed_host.clone(), self.parsed_port, &self.parsed_selector.clone(), false, ); /* to display the prompt */ loop { bunt::print!( "{[blue]}:{[blue]}{[blue]} ", self.current_host, self.current_port, &self.current_selector ); let line = input(""); match line.chars().next() { Some('?') => { bunt::println!( "{$yellow}?{/$} help\ \n{$yellow}*{/$} reload directory\ \n{$yellow}<{/$} go back in history\ \n{$yellow}.[LINK]{/$} download the given link\ \n{$yellow}H{/$} show history\ \n{$yellow}H[LINK]{/$} jump to the specified history item\ \n{$yellow}G[URI]{/$} jump to the given gopher URI\ \n{$yellow}B{/$} show bookmarks\ \n{$yellow}B[LINK]{/$} jump to the specified bookmark item\ \n{$yellow}C^d{/$} quit"); } Some('<') => { self.pop_history(); } Some('*') => { self.view_directory( &self.current_host.clone(), self.current_port, &self.current_selector.clone(), false, ); } Some('.') => { self.download_link( make_key( line.chars().next().unwrap(), line.chars().nth(1).unwrap_or('\0'), line.chars().nth(2).unwrap_or('\0')) .unwrap_or(0), ); } Some('H') => if i == 1 || i == 3 || i == 4 { self.view_history(make_key( line.chars().next().unwrap(), line.chars().nth(1).unwrap_or('\0'), line.chars().nth(2).unwrap_or('\0'), )); }, Some('G') => { if self.parse_uri(&line[1..]) { self.view_directory( &self.parsed_host.clone(), self.parsed_port, &self.parsed_selector.clone(), true, ); } else { println!("invalid gopher URI"); } } Some('B') => if i == 1 || i == 3 || i == 4 { self.view_bookmarks(make_key( line.chars().next().unwrap(), line.chars().nth(1).unwrap_or('\0'), line.chars().nth(2).unwrap_or('\0'), )); }, x if x.is_some() => { self.follow_link( make_key( line.chars().next().unwrap(), line.chars().nth(1).unwrap_or('\0'), line.chars().nth(2).unwrap_or('\0')) .unwrap_or(0), ); } _ => (), } } } } fn main() { let mut state = BrowserState::new(); let args: Vec = env::args().collect(); exit(state.init(args)) }