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.
phetch/src/gopher.rs

257 lines
8.1 KiB
Rust

5 years ago
use gopher;
use std::io;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::net::ToSocketAddrs;
use std::time::Duration;
pub const TCP_TIMEOUT_IN_SECS: u64 = 1;
pub const TCP_TIMEOUT_DURATION: Duration = Duration::from_secs(TCP_TIMEOUT_IN_SECS);
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum Type {
5 years ago
Text, // 0 | 96 | cyan
Menu, // 1 | 94 | blue
CSOEntity, // 2
Error, // 3 | 91 | red
Binhex, // 4 | 4 | white underline
DOSFile, // 5 | 4 | white underline
UUEncoded, // 6 | 4 | white underline
Search, // 7 | 0 | white
Telnet, // 8
Binary, // 9 | 4 | white underline
Mirror, // +
GIF, // g | 4 | white underline
Telnet3270, // T
HTML, // h | 92 | green
5 years ago
Image, // I | 4 | white underline
5 years ago
PNG, // p | 4 | white underline
5 years ago
Info, // i | 93 | yellow
Sound, // s | 4 | white underline
Document, // d | 4 | white underline
5 years ago
}
impl Type {
pub fn is_download(self) -> bool {
5 years ago
match self {
Type::Binhex
| Type::DOSFile
| Type::UUEncoded
| Type::Binary
| Type::GIF
5 years ago
| Type::Image
5 years ago
| Type::PNG
5 years ago
| Type::Sound
| Type::Document => true,
_ => false,
}
}
}
5 years ago
pub fn type_for_char(c: char) -> Option<Type> {
match c {
'0' => Some(Type::Text),
'1' => Some(Type::Menu),
'2' => Some(Type::CSOEntity),
'3' => Some(Type::Error),
'4' => Some(Type::Binhex),
'5' => Some(Type::DOSFile),
'6' => Some(Type::UUEncoded),
'7' => Some(Type::Search),
'8' => Some(Type::Telnet),
'9' => Some(Type::Binary),
'+' => Some(Type::Mirror),
'g' => Some(Type::GIF),
'T' => Some(Type::Telnet3270),
'h' => Some(Type::HTML),
5 years ago
'I' => Some(Type::Image),
5 years ago
'p' => Some(Type::PNG),
5 years ago
'i' => Some(Type::Info),
's' => Some(Type::Sound),
'd' => Some(Type::Document),
_ => None,
}
}
// produces an io::Error more easily
pub fn io_error(msg: String) -> io::Error {
io::Error::new(io::ErrorKind::Other, msg)
}
5 years ago
// Fetches a gopher URL and returns a raw Gopher response.
5 years ago
pub fn fetch_url(url: &str) -> io::Result<String> {
let (_, host, port, sel) = parse_url(url);
fetch(host, port, sel)
}
5 years ago
// Fetches a gopher URL by its component parts and returns a raw Gopher response.
pub fn fetch(host: &str, port: &str, selector: &str) -> io::Result<String> {
let mut body = String::new();
5 years ago
let selector = selector.replace('?', "\t"); // search queries
format!("{}:{}", host, port)
.to_socket_addrs()
.and_then(|mut socks| {
socks
.next()
5 years ago
.ok_or_else(|| io_error("Can't create socket".to_string()))
.and_then(|sock| {
TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION)
.and_then(|mut stream| {
stream.write(format!("{}\r\n", selector).as_ref());
Ok(stream)
})
.and_then(|mut stream| {
stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION));
stream.read_to_string(&mut body)?;
Ok(body)
})
})
})
}
5 years ago
// Downloads a binary to disk and returns the path it was saved to.
pub fn download_url(url: &str) -> io::Result<String> {
let (_, host, port, sel) = parse_url(url);
let sel = sel.replace('?', "\t"); // search queries
5 years ago
let parts = sel.split_terminator("/").collect::<Vec<&str>>();
let filename = parts.iter().rev().nth(0).unwrap_or(&"download");
let mut path = std::path::PathBuf::from(".");
path.push(filename);
format!("{}:{}", host, port)
.to_socket_addrs()
.and_then(|mut socks| {
socks
.next()
.ok_or_else(|| io_error("Can't create socket".to_string()))
.and_then(|sock| {
TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION)
.and_then(|mut stream| {
stream.write(format!("{}\r\n", sel).as_ref());
Ok(stream)
})
.and_then(|mut stream| {
stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION));
std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(path)
.and_then(|mut file| {
let mut buf = [0; 1024];
5 years ago
while let Ok(count) = stream.read(&mut buf) {
if count == 0 {
break;
}
file.write_all(&buf);
}
5 years ago
Ok(filename.to_string())
})
})
})
})
}
5 years ago
// url parsing states
5 years ago
enum Parsing {
Host,
Port,
Selector,
}
// Parses gopher URL into parts.
pub fn parse_url(url: &str) -> (Type, &str, &str, &str) {
5 years ago
let url = url.trim_start_matches("gopher://");
let mut host = "";
let mut port = "70";
let mut sel = "/";
5 years ago
let mut typ = Type::Menu;
5 years ago
let mut state = Parsing::Host;
let mut start = 0;
for (i, c) in url.char_indices() {
match state {
Parsing::Host => {
5 years ago
state = match c {
':' => Parsing::Port,
'/' => Parsing::Selector,
5 years ago
_ => continue,
5 years ago
};
5 years ago
host = &url[start..i];
5 years ago
start = if c == '/' { i } else { i + 1 };
5 years ago
}
Parsing::Port => {
if c == '/' {
state = Parsing::Selector;
port = &url[start..i];
5 years ago
start = i;
5 years ago
}
}
Parsing::Selector => {}
}
}
match state {
Parsing::Selector => sel = &url[start..],
Parsing::Port => port = &url[start..],
Parsing::Host => host = &url[start..],
};
let mut chars = sel.chars();
5 years ago
if let (Some('/'), Some(c), Some('/')) = (chars.nth(0), chars.nth(0), chars.nth(0)) {
if let Some(t) = gopher::type_for_char(c) {
typ = t;
sel = &sel[2..];
5 years ago
}
}
(typ, host, port, sel)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_parse() {
let urls = vec![
"gopher://gopher.club/1/phlogs/",
"gopher://sdf.org:7777/1/maps",
"gopher.floodgap.org",
"gopher.floodgap.com/0/gopher/relevance.txt",
"gopher://gopherpedia.com/7/lookup?Gopher",
];
let (typ, host, port, sel) = parse_url(urls[0]);
assert_eq!(typ, Type::Menu);
assert_eq!(host, "gopher.club");
assert_eq!(port, "70");
assert_eq!(sel, "/phlogs/");
let (typ, host, port, sel) = parse_url(urls[1]);
assert_eq!(typ, Type::Menu);
assert_eq!(host, "sdf.org");
assert_eq!(port, "7777");
assert_eq!(sel, "/maps");
let (typ, host, port, sel) = parse_url(urls[2]);
assert_eq!(typ, Type::Menu);
assert_eq!(host, "gopher.floodgap.org");
assert_eq!(port, "70");
assert_eq!(sel, "/");
let (typ, host, port, sel) = parse_url(urls[3]);
assert_eq!(typ, Type::Text);
assert_eq!(host, "gopher.floodgap.com");
assert_eq!(port, "70");
assert_eq!(sel, "/gopher/relevance.txt");
let (typ, host, port, sel) = parse_url(urls[4]);
assert_eq!(typ, Type::Search);
assert_eq!(host, "gopherpedia.com");
assert_eq!(port, "70");
assert_eq!(sel, "/lookup?Gopher");
}
}