Browse Source

Open any mime type.

Reworked parser to split header from content directly at the bytes
level.
Open files by downloading them to a temp file before opening them.
Refactor non-Cursive functions into other modules.
openbsd 0.6.0
Julien Blanchard 7 years ago
parent
commit
32d9aece61
  1. 32
      Cargo.lock
  2. 5
      Cargo.toml
  3. 89
      src/absolute.rs
  4. 25
      src/content.rs
  5. 5
      src/link.rs
  6. 294
      src/main.rs
  7. 2
      src/status.rs

32
Cargo.lock generated

@ -20,12 +20,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "asuka"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"cursive 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"json 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"open 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -109,7 +112,7 @@ dependencies = [
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"enum-map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"enumset 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"enumset 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
@ -187,16 +190,16 @@ dependencies = [
[[package]]
name = "enumset"
version = "0.4.0"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"enumset_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"enumset_derive 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "enumset_derive"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"darling 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -253,6 +256,11 @@ dependencies = [
"unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "json"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -380,6 +388,14 @@ dependencies = [
"autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "open"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl"
version = "0.10.24"
@ -777,8 +793,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum enum-map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bc0515b284e6ce2cbacd123b339d9c5a0ce49059baa4d9e584ab3803b3dc973"
"checksum enum-map-derive 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e57001dfb2532f5a103ff869656887fae9a8defa7d236f3e39d2ee86ed629ad7"
"checksum enum-map-internals 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8d7f81655c75281b36ddb9c2a1502afcac9db780859cc5b2eba08efcccb4c510"
"checksum enumset 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cac0a22e173f6570a7d69a2ab9e3fe79cf0dcdd0fdb162bfc932b97158f2b2a7"
"checksum enumset_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "01d93b926a992a4a526c2a14e2faf734fdef5bf9d0a52ba69a2ca7d4494c284b"
"checksum enumset 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4293261d4f3472132ffdeb1c97be5f5de5267c4a764c6cc10066aeff35a54c"
"checksum enumset_derive 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aeece157d0a6cda3f6015d7f16c570d4ba958161477448a9a6ec49851ccd8ee0"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
@ -786,6 +802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e1de41fb8dba9714efd92241565cdff73f78508c95697dd56787d3cba27e2353"
"checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
"checksum json 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3ca41abbeb7615d56322a984e63be5e5d0a117dfaca86c14393e32a762ccac1"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba"
@ -801,6 +818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum num-iter 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "76bd5272412d173d6bf9afdf98db8612bbabc9a7a830b7bfc9c188911716132e"
"checksum num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2885278d5fe2adc2f75ced642d52d879bffaceb5a2e0b1d4309ffdfb239b454"
"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
"checksum open 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "94b424e1086328b0df10235c6ff47be63708071881bead9e76997d9291c0134b"
"checksum openssl 0.10.24 (registry+https://github.com/rust-lang/crates.io-index)" = "8152bb5a9b5b721538462336e3bef9a539f892715e5037fda0f984577311af15"
"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
"checksum openssl-sys 0.9.49 (registry+https://github.com/rust-lang/crates.io-index)" = "f4fad9e54bd23bd4cbbe48fdc08a1b8091707ac869ef8508edea2fec77dcc884"

5
Cargo.toml

@ -1,6 +1,6 @@
[package]
name = "asuka"
version = "0.5.0"
version = "0.6.0"
authors = ["Julien Blanchard <julien@sideburns.eu>"]
edition = "2018"
@ -10,3 +10,6 @@ native-tls = "*"
url = "*"
regex = "*"
lazy_static = "*"
open = "*"
json = "*"
tempfile = "*"

89
src/absolute.rs

@ -0,0 +1,89 @@
use url::Url;
pub fn make(url: &str) -> Result<url::Url, url::ParseError> {
// Creates an absolute link if needed
match super::history::get_current_host() {
Some(host) => {
if url.starts_with("gemini://") {
Url::parse(url)
} else if url.starts_with("//") {
Url::parse(&format!("gemini:{}", url))
} else if url.starts_with('/') {
Url::parse(&format!("gemini://{}{}", host, url))
} else {
let current_host_path = super::history::get_current_url().unwrap();
Url::parse(&format!("{}{}", current_host_path, url))
}
}
None => {
if url.starts_with("gemini://") {
Url::parse(url)
} else if url.starts_with("//") {
Url::parse(&format!("gemini:{}", url))
} else {
Url::parse(&format!("gemini://{}", url))
}
}
}
}
#[test]
fn test_make_absolute_full_url() {
super::history::append("gemini://typed-hole.org");
let url = "gemini://typed-hole.org/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_full_url_no_protocol() {
super::history::append("gemini://typed-hole.org");
let url = "//typed-hole.org/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_slash_path() {
super::history::append("gemini://typed-hole.org");
let url = "/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_just_path() {
super::history::append("gemini://typed-hole.org");
let url = "foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_full_url_no_current_host() {
let url = "gemini://typed-hole.org/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_full_url_no_protocol_no_current_host() {
let url = "//typed-hole.org/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_slash_path_no_current_host() {
let url = "/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_just_path_no_current_host() {
let url = "foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}

25
src/content.rs

@ -1,10 +1,11 @@
use tempfile::NamedTempFile;
use std::io::{Read, Write};
use native_tls::TlsConnector;
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
pub fn get_data(url: &url::Url) -> Result<String, String> {
pub fn get_data(url: &url::Url) -> Result<(Vec<u8>, Vec<u8>), String> {
let host = url.host_str().unwrap();
let urlf = format!("{}:1965", host);
@ -28,7 +29,11 @@ pub fn get_data(url: &url::Url) -> Result<String, String> {
stream.write_all(url.as_bytes()).unwrap();
let mut res = vec![];
stream.read_to_end(&mut res).unwrap();
Ok(String::from_utf8_lossy(&res).to_string())
let clrf_idx = find_subsequence(&res, b"\r\n");
let content = res.split_off(clrf_idx.unwrap() + 2);
Ok((res, content))
}
Err(e) => Err(format!("Could not connect to {}\n{}", urlf, e)),
}
@ -41,3 +46,19 @@ pub fn get_data(url: &url::Url) -> Result<String, String> {
Err(e) => Err(format!("Could not connect to {}\n{}", urlf, e))
}
}
pub fn download(content: Vec<u8>) {
let path = write_tmp_file(content);
open::that(path).unwrap();
}
fn write_tmp_file(content: Vec<u8>) -> std::path::PathBuf {
let mut tmp_file = NamedTempFile::new().unwrap();
tmp_file.write_all(&content).unwrap();
let (_file, path) = tmp_file.keep().unwrap();
path
}
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|window| window == needle)
}

5
src/link.rs

@ -2,6 +2,7 @@ extern crate regex;
use regex::Regex;
use url::Url;
use std::str::FromStr;
use json::JsonValue;
#[derive(Debug)]
pub enum Link {
@ -64,3 +65,7 @@ fn make_link(url: String, label: String) -> Option<Link> {
_ => None
}
}
pub fn is_gemini(line: &JsonValue) -> bool {
line["type"] == "gemini"
}

294
src/main.rs

@ -5,13 +5,15 @@ extern crate native_tls;
extern crate regex;
use cursive::align::HAlign;
use cursive::theme::Effect;
use cursive::theme::{BaseColor, Color, Effect, PaletteColor, Style, Theme};
use cursive::traits::*;
use cursive::utils::markup::StyledString;
use cursive::view::Scrollable;
use cursive::views::{Dialog, EditView, Panel, SelectView};
use cursive::Cursive;
use json::object;
use std::str::FromStr;
use url::Url;
@ -23,12 +25,13 @@ use link::Link;
mod content;
mod history;
mod absolute;
const HELP: &str = "Welcome to Asuka Gemini browser!
Press g to visit an URL
Press h to show/hide history
Press q to exit
Press g to visit an URL
Press h to show/hide history
Press q to exit
";
fn main() {
@ -36,11 +39,14 @@ fn main() {
let mut siv = Cursive::default();
let theme = custom_colors(&siv);
siv.set_theme(theme);
let mut select = SelectView::new();
select.add_all_str(HELP.lines());
select.set_on_submit(|s, link| {
follow_link(s, link);
select.set_on_submit(|s, line| {
follow_line(s, line);
});
siv.add_fullscreen_layer(
@ -49,7 +55,9 @@ fn main() {
))
.title("Asuka Browser")
.h_align(HAlign::Center)
.button("Quit", |s| s.quit())
.button("Go To URL (g)", |s| prompt_for_url(s))
.button("History (h)", |s| show_history(s))
.button("Quit (q)", |s| s.quit())
.with_id("container"),
);
@ -63,6 +71,13 @@ fn main() {
siv.run();
}
fn custom_colors(s: &Cursive) -> Theme {
// We'll return the current theme with a small modification.
let mut theme = s.current_theme().clone();
theme.palette[PaletteColor::Highlight] = Color::Rgb(120, 120, 120);
theme
}
fn prompt_for_url(s: &mut Cursive) {
s.add_layer(
Dialog::new()
@ -83,10 +98,11 @@ fn prompt_for_answer(s: &mut Cursive, url: Url, message: String) {
.content(
EditView::new()
.on_submit(move |s, response| {
let link = format!("{}?query={}", url.to_string(), response);
let link = format!("{}?{}", url.to_string(), response);
s.pop_layer();
follow_link(s, &link);
}).fixed_width(60)
})
.fixed_width(60),
)
.with_id("url_query"),
);
@ -134,14 +150,16 @@ fn visit_url(s: &mut Cursive, url: &Url) {
s.pop_layer();
}
match make_absolute(url.as_str()) {
Ok(url) => match content::get_data(&url) {
Ok(new_content) => {
history::append(url.as_str());
draw_content(s, url, new_content);
}
Err(msg) => {
s.add_layer(Dialog::info(msg));
match absolute::make(url.as_str()) {
Ok(url) => {
match content::get_data(&url) {
Ok((meta, new_content)) => {
history::append(url.as_str());
draw_content(s, &url, meta, new_content);
}
Err(msg) => {
s.add_layer(Dialog::info(msg));
}
}
},
Err(_) => {
@ -150,81 +168,78 @@ fn visit_url(s: &mut Cursive, url: &Url) {
}
}
fn draw_content(s: &mut Cursive, url: Url, content: String) {
let url_copy = url.clone();
fn draw_content(s: &mut Cursive, url: &Url, meta: Vec<u8>, content: Vec<u8>) {
let content_str = String::from_utf8_lossy(&content).to_string();
// handle response status
if let Some(status_line) = content.lines().next() {
if let Ok(status) = Status::from_str(status_line) {
match status {
Status::Success(_meta) => {}
Status::Gone(_meta) => {
s.add_layer(Dialog::info("Sorry page is gone."));
return;
}
Status::RedirectTemporary(new_url) | Status::RedirectPermanent(new_url) => {
return follow_link(s, &new_url)
}
Status::TransientCertificateRequired(_meta)
| Status::AuthorisedCertificatedRequired(_meta) => {
s.add_layer(Dialog::info(
"You need a valid certificate to access this page.",
));
return;
}
Status::Input(message) => {
prompt_for_answer(s, url_copy, message);
}
other_status => {
s.add_layer(Dialog::info(format!("ERROR: {:?}", other_status)));
return;
}
}
}
}
// handle meta header
handle_response_status(s, url, meta, content);
let mut main_view = match s.find_id::<SelectView>("main") {
Some(view) => view,
None => panic!("Can't find main view.")
};
let mut container = match s.find_id::<Dialog>("container") {
Some(view) => view,
None => panic!("Can't find container view.")
None => panic!("Can't find main view."),
};
// set title and clear old content
container.set_title(url.as_str());
set_title(s, url.as_str());
main_view.clear();
// draw new content lines
for line in content.lines().skip(1) {
for line in content_str.lines() {
match Link::from_str(line) {
Ok(link) => match link {
Link::Http(_url, label) => {
Link::Http(url, label) => {
let mut formatted = StyledString::new();
let www_label = format!("[WWW] {}", label);
formatted.append(StyledString::styled(www_label, Effect::Italic));
let www_label = format!("{} [WWW]", label);
formatted.append(StyledString::styled(
www_label,
Style::from(Color::Dark(BaseColor::Green)).combine(Effect::Bold),
));
main_view.add_item(formatted, String::from("0"))
let data = object! {
"type" => "www",
"url" => url.to_string()
};
main_view.add_item(formatted, json::stringify(data))
}
Link::Gopher(_url, label) => {
Link::Gopher(url, label) => {
let mut formatted = StyledString::new();
let gopher_label = format!("[Gopher] {}", label);
formatted.append(StyledString::styled(gopher_label, Effect::Italic));
let gopher_label = format!("{} [Gopher]", label);
formatted.append(StyledString::styled(
gopher_label,
Style::from(Color::Light(BaseColor::Magenta)).combine(Effect::Bold),
));
main_view.add_item(formatted, String::from("0"))
let data = object! {
"type" => "gopher",
"url" => url.to_string()
};
main_view.add_item(formatted, json::stringify(data))
}
Link::Gemini(url, label) => {
let mut formatted = StyledString::new();
formatted.append(StyledString::styled(label, Effect::Underline));
formatted.append(StyledString::styled(
label,
Style::from(Color::Light(BaseColor::Blue)).combine(Effect::Bold),
));
main_view.add_item(formatted, url.to_string())
let data = object! {
"type" => "gemini",
"url" => url.to_string()
};
main_view.add_item(formatted, json::stringify(data))
}
Link::Relative(url, label) => {
let mut formatted = StyledString::new();
formatted.append(StyledString::styled(label, Effect::Underline));
formatted.append(StyledString::styled(
label,
Style::from(Color::Light(BaseColor::Blue)).combine(Effect::Bold),
));
main_view.add_item(formatted, url.to_string())
let data = object! {
"type" => "gemini",
"url" => url.to_string()
};
main_view.add_item(formatted, json::stringify(data))
}
Link::Unknown(_, _) => (),
},
@ -233,101 +248,72 @@ fn draw_content(s: &mut Cursive, url: Url, content: String) {
}
}
fn is_gemini_link(line: &str) -> bool {
line != "0"
fn handle_response_status(s: &mut Cursive, url: &Url, meta: Vec<u8>, content: Vec<u8>) {
let url_copy = url.clone();
let meta_str = String::from_utf8_lossy(&meta).to_string();
if let Ok(status) = Status::from_str(&meta_str) {
match status {
Status::Success(meta) => {
if meta.starts_with("text/") {
// display text files.
{}
} else {
// download and try to open the rest.
content::download(content);
return;
}
}
Status::Gone(_meta) => {
s.add_layer(Dialog::info("Sorry page is gone."));
return;
}
Status::RedirectTemporary(new_url) | Status::RedirectPermanent(new_url) => {
return follow_link(s, &new_url)
}
Status::TransientCertificateRequired(_meta)
| Status::AuthorisedCertificatedRequired(_meta) => {
s.add_layer(Dialog::info(
"You need a valid certificate to access this page.",
));
return;
}
Status::Input(message) => {
prompt_for_answer(s, url_copy, message);
}
other_status => {
s.add_layer(Dialog::info(format!("ERROR: {:?}", other_status)));
return;
}
}
}
}
fn follow_link(s: &mut Cursive, line: &str) {
if is_gemini_link(line) {
let next_url = make_absolute(line).expect("Not an URL");
visit_url(s, &next_url)
}
fn set_title(s: &mut Cursive, text: &str) {
let mut container = match s.find_id::<Dialog>("container") {
Some(view) => view,
None => panic!("Can't find container view."),
};
container.set_title(text);
}
fn make_absolute(url: &str) -> Result<url::Url, url::ParseError> {
// Creates an absolute link if needed
match history::get_current_host() {
Some(host) => {
if url.starts_with("gemini://") {
Url::parse(url)
} else if url.starts_with("//") {
Url::parse(&format!("gemini:{}", url))
} else if url.starts_with('/') {
Url::parse(&format!("gemini://{}{}", host, url))
} else {
let current_host_path = history::get_current_url().unwrap();
Url::parse(&format!("{}{}", current_host_path, url))
}
}
None => {
if url.starts_with("gemini://") {
Url::parse(url)
} else if url.starts_with("//") {
Url::parse(&format!("gemini:{}", url))
fn follow_line(s: &mut Cursive, line: &str) {
let parsed = json::parse(line);
match parsed {
Ok(data) => {
if link::is_gemini(&data) {
let next_url = absolute::make(&data["url"].to_string()).expect("Not an URL");
visit_url(s, &next_url)
} else {
Url::parse(&format!("gemini://{}", url))
open::that(data["url"].to_string()).unwrap();
}
}
Err(_) => (),
}
}
#[test]
fn test_make_absolute_full_url() {
history::append("gemini://typed-hole.org");
let url = "gemini://typed-hole.org/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make_absolute(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_full_url_no_protocol() {
history::append("gemini://typed-hole.org");
let url = "//typed-hole.org/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make_absolute(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_slash_path() {
history::append("gemini://typed-hole.org");
let url = "/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make_absolute(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_just_path() {
history::append("gemini://typed-hole.org");
let url = "foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make_absolute(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_full_url_no_current_host() {
let url = "gemini://typed-hole.org/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make_absolute(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_full_url_no_protocol_no_current_host() {
let url = "//typed-hole.org/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make_absolute(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_slash_path_no_current_host() {
let url = "/foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make_absolute(&url).unwrap();
assert_eq!(expected_url, absolute_url);
}
#[test]
fn test_make_absolute_just_path_no_current_host() {
let url = "foo";
let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
let absolute_url = make_absolute(&url).unwrap();
assert_eq!(expected_url, absolute_url);
fn follow_link(s: &mut Cursive, link: &str) {
let next_url = absolute::make(link).expect("Not an URL");
visit_url(s, &next_url)
}

2
src/status.rs

@ -32,7 +32,7 @@ pub enum Status {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ParseError;
const STATUS_REGEX: &str = r"^(\d{1,3})[ \t](.*)$";
const STATUS_REGEX: &str = r"^(\d{1,3})[ \t](.*)\r\n$";
impl FromStr for Status {
type Err = ParseError;

Loading…
Cancel
Save