You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

685 lines
16 KiB
Rust

#![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<Box<Link>>,
pub which: Option<char>,
pub key: usize,
pub host: String,
pub port: u16,
pub selector: String,
}
#[derive(Clone, Debug)]
pub struct BrowserState {
pub tmpfilename: String,
pub links: Option<Box<Link>>,
pub history: Option<Box<Link>>,
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<String>,
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::<Vec<_>>()
};
/* 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::<String>()
);
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<usize>) {
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<Box<Link>>;
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<usize>) {
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<Box<Link>> = 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<Box<Link>> = 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<String>) -> 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<String> = env::args().collect();
exit(state.init(args))
}