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.
834 lines
20 KiB
Rust
834 lines
20 KiB
Rust
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::{Write, BufReader, BufRead, Read, self};
|
|
use std::net::TcpStream;
|
|
|
|
use simple_input::input;
|
|
use tempfile::NamedTempFile;
|
|
|
|
/* structs */
|
|
#[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 Config {
|
|
pub start_uri: String,
|
|
pub cmd_text: String,
|
|
pub cmd_image: String,
|
|
pub cmd_browser: String,
|
|
pub cmd_player: String,
|
|
pub color_prompt: String,
|
|
pub color_selector: String,
|
|
pub verbose: bool,
|
|
}
|
|
|
|
#[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 {
|
|
start_uri: String::from("gopher://gopher.floodgap.com:70"),
|
|
cmd_text: String::from("less"),
|
|
cmd_image: String::from("display"),
|
|
cmd_browser: String::from("firefox"),
|
|
cmd_player: String::from("mplayer"),
|
|
color_prompt: String::from("1;34"),
|
|
color_selector: String::from("1;32"),
|
|
verbose: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn parse_config_line(&mut self, line: &str) {
|
|
let trimmed_line = line.trim_start();
|
|
|
|
let words = trimmed_line.split_whitespace().take(2).collect::<Vec<_>>();
|
|
match words[0] {
|
|
"start_uri" => self.config.start_uri = words[1].to_string(),
|
|
"cmd_text" => self.config.cmd_text = words[1].to_string(),
|
|
"cmd_browser" => self.config.cmd_browser = words[1].to_string(),
|
|
"cmd_image" => self.config.cmd_image = words[1].to_string(),
|
|
"cmd_player" => self.config.cmd_player = words[1].to_string(),
|
|
"color_prompt" => self.config.color_prompt = words[1].to_string(),
|
|
"color_selector" => self.config.color_selector = words[1].to_string(),
|
|
"verbose" => self.config.verbose = words[1].parse().unwrap_or_default(),
|
|
x if x.starts_with("bookmark") => self.bookmarks.push(words[1].to_string()),
|
|
x => {
|
|
eprintln!("invalid key in config: {}", x);
|
|
exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn load_config<P: AsRef<Path>>(&mut self, filename: P) {
|
|
let file = match File::open(filename) {
|
|
Ok(f) => BufReader::new(f),
|
|
Err(_) => {
|
|
return;
|
|
}
|
|
};
|
|
file.lines()
|
|
.filter_map(|x| x.ok())
|
|
.filter(|x| !x.starts_with('#'))
|
|
.for_each(|line| self.parse_config_line(&line))
|
|
}
|
|
|
|
pub fn init_config(&mut self) {
|
|
/* read configs */
|
|
self.load_config("/etc/cgorc"); /* ignore incomplete selectors */
|
|
if let Some(dir) = dirs::home_dir() {
|
|
self.load_config(dir.join(".cgorc"));
|
|
}
|
|
}
|
|
|
|
pub fn download_file(
|
|
&mut 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;
|
|
}
|
|
return 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 = {
|
|
let mut v = line[1..].split('\t').collect::<Vec<_>>();
|
|
v.retain(|x| !x.is_empty());
|
|
v
|
|
};
|
|
/* 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!(
|
|
// "error: not a directory: [{}] {}",
|
|
// line.chars().next().unwrap_or('\0'),
|
|
// line.chars().skip(1).collect::<String>()
|
|
// );
|
|
// return;
|
|
// }
|
|
|
|
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;
|
|
}
|
|
};
|
|
// O_CREAT, O_NOCTTY, O_WRONLY, O_EXCL
|
|
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 = history_key + 1;
|
|
make_key_str(fresh10, &mut a, &mut b, &mut c);
|
|
println!("{}{}{} {}:{}/{}", 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.clone(),
|
|
&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;
|
|
}
|
|
/* return the array is broken after view! */
|
|
}
|
|
return 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, argc: usize, argv: Vec<String>) -> i32 {
|
|
let mut i: usize = 1;
|
|
/* copy defaults */
|
|
self.init_config();
|
|
let mut uri: String = self.config.start_uri.clone();
|
|
/* parse command line */
|
|
while i < argc {
|
|
if argv[i].chars().next() == Some('-') {
|
|
match argv[i].chars().skip(1).next() {
|
|
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.clone(),
|
|
&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('?') => {
|
|
println!("? - help\n* - reload directory\n< - go back in history\n.[LINK] - download the given link\nH - show history\nH[LINK] - jump to the specified history item\nG[URI] - jump to the given gopher URI\nB - show bookmarks\nB[LINK] - jump to the specified bookmark item\nC^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().skip(1).next().unwrap_or('\0'),
|
|
line.chars().skip(2).next().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().skip(1).next().unwrap_or('\0'),
|
|
line.chars().skip(2).next().unwrap_or('\0'),
|
|
));
|
|
},
|
|
Some('G') => {
|
|
if self.parse_uri(&line[1..]) {
|
|
self.view_directory(
|
|
&self.parsed_host.clone(),
|
|
self.parsed_port.clone(),
|
|
&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().skip(1).next().unwrap_or('\0'),
|
|
line.chars().skip(2).next().unwrap_or('\0'),
|
|
));
|
|
},
|
|
_ => {
|
|
self.follow_link(
|
|
make_key(
|
|
line.chars().next().unwrap(),
|
|
line.chars().skip(1).next().unwrap_or('\0'),
|
|
line.chars().skip(2).next().unwrap_or('\0'))
|
|
.unwrap_or(0),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
/* never get's here but stops cc complaining */
|
|
}
|
|
}
|
|
|
|
/* implementation */
|
|
pub fn usage() {
|
|
eprintln!("usage: cgo [-v] [-H] [gopher URI]");
|
|
exit(0);
|
|
}
|
|
pub fn banner(to_error: bool) {
|
|
if to_error {
|
|
eprintln!("cgo 0.6.1 Copyright (c) 2020 Sebastian Steinhauer");
|
|
} else {
|
|
println!("cgo 0.6.1 Copyright (c) 2020 Sebastian Steinhauer");
|
|
}
|
|
}
|
|
|
|
pub fn dial(
|
|
host: &str,
|
|
port: u16,
|
|
selector: &str,
|
|
) -> Option<TcpStream> {
|
|
let mut stream = match TcpStream::connect((host, port)) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("failed to dial server: {}", e);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
if let Err(e) = writeln!(stream, "{}", selector) {
|
|
eprintln!("failed to send request to server: {}", e);
|
|
return None;
|
|
};
|
|
|
|
Some(stream)
|
|
}
|
|
|
|
pub fn make_key(c1: char, c2: char, c3: char) -> Option<usize> {
|
|
if c1 == '\0' || c2 == '\0' {
|
|
return None;
|
|
}
|
|
if c3 == '\0' {
|
|
Some(
|
|
(c1 as u8 - 'a' as u8) as usize * ('z' as usize - 'a' as usize + 1)
|
|
+ (c2 as u8 - 'a' as u8) as usize,
|
|
)
|
|
} else {
|
|
Some(
|
|
((c1 as u8 - 'a' as u8) as usize + 1)
|
|
* ('z' as usize - 'a' as usize + 1)
|
|
* ('z' as usize - 'a' as usize + 1)
|
|
+ ((c2 as u8 - 'a' as u8) as usize) * ('z' as usize - 'a' as usize + 1)
|
|
+ ((c3 as u8 - 'a' as u8) as usize),
|
|
)
|
|
}
|
|
}
|
|
pub fn make_key_str(
|
|
key: usize,
|
|
c1: &mut char,
|
|
c2: &mut char,
|
|
c3: &mut char,
|
|
) {
|
|
if key
|
|
< ('z' as usize - 'a' as usize + 1 as usize)
|
|
* ('z' as usize - 'a' as usize + 1 as usize)
|
|
{
|
|
*c1 = ('a' as usize + key / ('z' as usize - 'a' as usize + 1 as usize)) as u8
|
|
as char;
|
|
*c2 = ('a' as usize + key % ('z' as usize - 'a' as usize + 1 as usize)) as u8
|
|
as char;
|
|
*c3 = 0 as usize as u8 as char
|
|
} else {
|
|
*c1 = ('a' as usize
|
|
+ key
|
|
/ (('z' as usize - 'a' as usize + 1 as usize)
|
|
* ('z' as usize - 'a' as usize + 1 as usize))
|
|
- 1 as usize) as u8 as char;
|
|
*c2 = ('a' as usize
|
|
+ key / ('z' as usize - 'a' as usize + 1 as usize)
|
|
% ('z' as usize - 'a' as usize + 1 as usize)) as u8 as char;
|
|
*c3 = ('a' as usize + key % ('z' as usize - 'a' as usize + 1 as usize)) as u8
|
|
as char
|
|
};
|
|
}
|
|
|
|
pub fn is_valid_directory_entry(kind: char) -> bool {
|
|
match kind {
|
|
'i' | '3' | '.' | '0' | '1' | '5' | '7' | '8' | '9' | 'g' | 'I' | 'p' | 'h'
|
|
| 's' => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn view_telnet(host: &str, port: u16) {
|
|
println!("executing: telnet {} {}", host, port);
|
|
|
|
// TODO check stdio
|
|
match Command::new("telnet").arg(host).arg(port.to_string()).spawn() {
|
|
Ok(mut c) =>
|
|
if let Err(e) = c.wait() {
|
|
eprintln!("failed to wait for telnet: {}", e);
|
|
},
|
|
Err(e) => eprintln!("failed to start telnet: {}", e),
|
|
}
|
|
println!("(done)");
|
|
}
|
|
|
|
fn main() {
|
|
let mut state = BrowserState::new();
|
|
let args: Vec<String> = env::args().collect();
|
|
exit(state.init(args.len() - 1, args) as i32)
|
|
}
|