#![warn(rust_2018_idioms)]
#![warn(missing_docs, missing_debug_implementations)]
#![warn(anonymous_parameters, bare_trait_objects, unreachable_pub)]
#![deny(unused)]
#![deny(unused_variables)]
#![forbid(unsafe_code)]
use async_std::io;
use async_std::net::TcpStream;
use async_tls::TlsConnector;
use futures::io::AsyncWriteExt;
use httparse::Header;
use httparse::Response;
use nom::bytes::complete::tag;
use nom::bytes::complete::take_until;
use nom::error::ErrorKind;
use nom::sequence::tuple;
use nom::IResult;
use rustls::ClientConfig;
use std::boxed::Box;
use std::convert::TryFrom;
use std::error::Error;
use std::io::Cursor;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::path::Path;
use std::path::PathBuf;
use std::str::from_utf8;
use std::sync::Arc;
use std::time::Duration;
use structopt::StructOpt;
use url::Host;
use url::ParseError;
use url::Url;
use x11_clipboard::Clipboard;
#[macro_use]
extern crate log;
#[allow(trivial_casts)]
const TITLE_TAG_OPEN: &str = "<title>";
const TITLE_TAG_CLOSE: &str = "</title>";
#[derive(Debug, StructOpt)]
pub struct Options {
#[structopt(short = "u", long = "url")]
pub url: Option<String>,
#[structopt(short = "c", long = "clipboard")]
pub clipboard: bool,
#[structopt(short = "h", long = "host")]
pub host: Option<String>,
#[structopt(short = "p", long = "port", default_value = "443")]
pub port: u16,
#[structopt(short = "s", long = "scheme", default_value = "https")]
pub scheme: String,
#[structopt(short = "t", long = "path", default_value = "/")]
pub path: String,
#[structopt(short = "q", long = "query")]
pub query: Option<String>,
#[structopt(short = "f", long = "fragment")]
pub fragment: Option<String>,
#[structopt(short = "d", long = "domain")]
pub domain: Option<String>,
#[structopt(short = "", long = "cafile", parse(from_os_str))]
pub cafile: Option<PathBuf>,
#[structopt(short = "r", long = "run")]
pub run: bool,
}
pub async fn connector(cafile: &Option<PathBuf>) -> Result<TlsConnector, Box<dyn Error>> {
if let Some(cafile) = cafile {
connector_for_ca_file(cafile).await.map_err(|ioerr| {
let e: Box<dyn Error> = Box::new(ioerr);
e
})
} else {
Ok(TlsConnector::default())
}
}
async fn connector_for_ca_file(cafile: &Path) -> Result<TlsConnector, io::Error> {
let mut config = ClientConfig::new();
let file = async_std::fs::read(cafile).await?;
let mut pem = Cursor::new(file);
config
.root_store
.add_pem_file(&mut pem)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))?;
Ok(TlsConnector::from(Arc::new(config)))
}
pub fn page_title_from_html(content: &[u8]) -> Result<Option<String>, Box<dyn Error>> {
let content: &str = match from_utf8(&content) {
Ok(v) => v,
Err(e) => {
let e: Box<dyn Error> = From::from(format!("Invalid UTF-8 sequence: {}", e));
return Err(e);
}
};
let has_tag: Result<(&str, &str), nom::Err<(&str, ErrorKind)>> = until_open_title_tag(content);
if let Ok((content, _before_tag)) = has_tag {
let parsed: IResult<&str, (_, _, &str), (&str, ErrorKind)> = tuple((
take_until(TITLE_TAG_OPEN),
tag(TITLE_TAG_OPEN),
take_until(TITLE_TAG_CLOSE),
))(content);
let (_, (_, _, title)) = match parsed {
Ok(r) => r,
Err(_) => {
let e: Box<dyn Error> = From::from("parsing error");
return Err(e);
}
};
return Ok(Some(String::from(title.trim())));
}
Ok(None)
}
fn until_open_title_tag(s: &str) -> IResult<&str, &str> {
take_until(TITLE_TAG_OPEN)(s)
}
pub fn has_redirect(
response: &Response<'_, '_>,
origin: &Url,
) -> Result<Option<Url>, Box<dyn Error>> {
match &response.code {
None => {
let e: Box<dyn Error> =
From::from("HTTP response parsing error: could not find StatusCode");
Err(e)
}
Some(c @ 300..=308) => {
for Header { name, value } in response.headers.iter() {
if name == &"Location" {
let dest: &str = from_utf8(&value).unwrap();
let dest: Url = match Url::parse(dest) {
Ok(url) => url,
Err(ParseError::RelativeUrlWithoutBase) => origin
.join(dest)
.expect("unparseable relative redirect destination"),
Err(_) => {
unreachable!();
}
};
debug!("HTTP redirect: {} -> {}", c, dest);
return Ok(Some(dest));
}
}
let e: Box<dyn Error> = From::from(format!("HTTP redirect without location: {}", c));
Err(e)
}
Some(c) => {
debug!("HTTP StatusCode: {}", c);
Ok(None)
}
}
}
#[derive(Debug)]
pub enum Jump {
Next(Url),
Landing(Vec<u8>),
}
pub async fn page_content(connector: &TlsConnector, parts: &Parts) -> Result<Jump, Box<dyn Error>> {
let socket_addr: &SocketAddr = &parts.addr;
let domain: &str = &parts.domain;
let tcp_stream: TcpStream = match TcpStream::connect(&socket_addr).await {
Ok(t) => t,
Err(e) => {
let e: Box<dyn Error> = From::from(format!("TCPStream error: {}", e));
return Err(e);
}
};
let mut tls_stream = match connector.connect(&domain, tcp_stream).await {
Ok(ts) => ts,
Err(e) => {
let e: Box<dyn Error> = From::from(format!("TLSStream error: {}", e));
return Err(e);
}
};
tls_stream
.write_all(parts.http_request().as_bytes())
.await?;
let mut response_content: Vec<u8> = Vec::with_capacity(1024);
io::copy(&mut tls_stream, &mut response_content).await?;
let mut headers: [Header<'_>; 32] = [httparse::EMPTY_HEADER; 32];
let mut response: Response<'_, '_> = Response::new(&mut headers);
response.parse(&response_content)?;
match has_redirect(&response, &parts.url)? {
Some(url) => Ok(Jump::Next(url)),
None => Ok(Jump::Landing(response_content)),
}
}
fn clipboard_content() -> Result<String, Box<dyn Error>> {
let clipboard = Clipboard::new().unwrap();
let clipboard_content = clipboard.load(
clipboard.setter.atoms.clipboard,
clipboard.setter.atoms.utf8_string,
clipboard.setter.atoms.property,
Duration::from_secs(3),
)?;
let clipboard_content = String::from_utf8(clipboard_content).expect("UTF-8 content");
debug!("clipboard content: {}", &clipboard_content);
Ok(clipboard_content)
}
#[derive(Clone, Debug)]
pub struct Parts {
pub url: Url,
pub host: Host<String>,
pub port: u16,
pub addr: SocketAddr,
pub domain: String,
pub path: String,
pub query: String,
pub fragment: String,
}
impl Parts {
pub fn http_request(&self) -> String {
let query = if self.query != "" && !self.query.starts_with('?') {
format!("?{}", self.query)
} else {
self.query.clone()
};
let fragment = if self.fragment != "" && !self.fragment.starts_with('#') {
format!("#{}", self.fragment)
} else {
self.fragment.clone()
};
let http_request: String = format!(
"GET {}{}{} HTTP/1.0\r\nHost: {}\r\n\r\n",
self.path, query, fragment, self.domain,
);
debug!("HTTP request: {}", &http_request);
http_request
}
}
impl TryFrom<&Options> for Parts {
type Error = Box<dyn Error>;
fn try_from(options: &Options) -> Result<Self, Self::Error> {
let mutex_opts: [bool; 3] = [
options.clipboard,
options.url.is_some(),
options.host.is_some(),
];
match mutex_opts {
[true, false, false] | [false, true, false] | [false, false, true] => {}
[false, false, false]
| [true, true, false]
| [true, false, true]
| [false, true, true]
| [true, true, true] => {
let e: Box<dyn Error> =
From::from("use one and only one of --clipboard, --url or --host");
return Err(e);
}
}
let url: Url = if options.clipboard {
Url::parse(&clipboard_content().expect("clipboard is not readable"))
.expect("Clipboard is not a URL")
} else if let Some(u) = &options.url {
Url::parse(&u).expect("Unparseable URL")
} else if let Some(h) = &options.host {
Url::parse(&format!("{}://{}", options.scheme, h)).expect("Unparseable scheme and host")
} else {
unreachable!("use only one of --clipboard, --url or --host")
};
let host: Host<String> = if let Some(h) = &options.host {
Host::parse(&h).expect("Unparseable Host")
} else {
url.host().expect("URL without host").to_owned()
};
debug!("host name: {}", &host);
let port: u16 = match options.port {
0 => match url.scheme() {
"https" => 443,
"http" => {
let e: Box<dyn Error> =
From::from("HTTP standard ports is not yet suported".to_string());
return Err(e);
}
s => {
let e: Box<dyn Error> = From::from(format!(
"Only HTTP(S) standard ports are suported. {} given",
s
));
return Err(e);
}
},
p => p,
};
debug!("port number: {}", &port);
let addr: SocketAddr = (host.to_string().as_str(), options.port)
.to_socket_addrs()?
.next()
.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?;
debug!("socket address: {}", &addr);
let domain: String = if let Some(d) = &options.domain {
d.to_owned()
} else {
host.to_string()
};
debug!("domain {}", &domain);
let path: String = match &options.host {
None => (&url.path()).to_string(),
_ => options.path.to_string(),
};
let query: String = match &options.query {
Some(q) => q.to_owned(),
None => url.query().unwrap_or("").to_string(),
};
let fragment: String = match &options.fragment {
Some(f) => f.to_owned(),
None => url.fragment().unwrap_or("").to_string(),
};
Ok(Parts {
url,
host,
port,
addr,
query,
fragment,
domain,
path,
})
}
}
impl TryFrom<&str> for Parts {
type Error = Box<dyn Error>;
fn try_from(url: &str) -> Result<Self, Self::Error> {
let url: Url = Url::parse(&url).expect("Unparseable URL");
Self::try_from(&url)
}
}
impl TryFrom<&Url> for Parts {
type Error = Box<dyn Error>;
fn try_from(url: &Url) -> Result<Self, Self::Error> {
let host: Host<String> = url.host().expect("URL without host").to_owned();
let port: u16 = match url.scheme() {
"https" => 443,
"http" => 80,
s => {
let e: Box<dyn Error> = From::from(format!(
"Only HTTP(S) standard ports are suported. {} given",
s
));
return Err(e);
}
};
let addr: SocketAddr = (host.to_string().as_str(), port)
.to_socket_addrs()?
.next()
.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?;
let domain: String = host.to_string();
let path: String = (&url.path()).to_string();
let query: String = url.query().unwrap_or("").to_string();
let fragment: String = url.fragment().unwrap_or("").to_string();
Ok(Parts {
url: url.clone(),
host,
port,
addr,
query,
fragment,
domain,
path,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_std::task;
use std::fs;
use std::time::Instant;
#[allow(dead_code)]
fn init() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[test]
fn test_connector_passes_without_cafile() -> Result<(), String> {
match task::block_on(async { connector(&None).await }) {
Ok(_) => Ok(()),
Err(_) => Err(String::from("connector should use the system CA files")),
}
}
#[test]
fn test_connector_fails_with_nonexisting_cafile() -> Result<(), String> {
let cafile: &Option<PathBuf> = &Some(PathBuf::from("/nope"));
match task::block_on(async { connector(cafile).await }) {
Ok(_) => Err(String::from(
"connector should fail when the CA file does not exist",
)),
Err(_) => Ok(()),
}
}
#[test]
#[should_panic(expected = "No such file or directory")]
fn test_connector_fails_with_nonexisting_cafile_with_failure_message() {
let cafile: &Option<PathBuf> = &Some(PathBuf::from("/nope"));
task::block_on(async { connector(cafile).await }).unwrap();
}
#[test]
fn test_connector_for_ca_file_passes() -> Result<(), String> {
let cafile: &Path =
&Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/files/doc_rust-lang_org.crt");
match task::block_on(async { connector_for_ca_file(cafile).await }) {
Ok(_) => Ok(()),
Err(_) => Err(String::from("could not load the test PEM certificate")),
}
}
#[test]
fn test_page_title_from_html_passes_with_valid_html() -> Result<(), String> {
let page: &Path =
&Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/files/home_with_title.html");
match page_title_from_html(
&fs::read(page).map_err(|_| String::from("could not open HTML test page"))?,
) {
Ok(Some(title)) if title == String::from("Rust Programming Language") => Ok(()),
Ok(Some(title)) => Err(format!("unexpected title: \"{}\"", title)),
Ok(None) => Err(From::from("could not find a title in the page")),
_ => Err(String::from(
"could not extract the title from the HTML page content",
)),
}
}
#[test]
fn test_page_title_from_html_passes_with_valid_html_without_title() -> Result<(), String> {
let page: &Path =
&Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/files/home_without_title.html");
match page_title_from_html(
&fs::read(page).map_err(|_| String::from("could not open HTML test page"))?,
) {
Ok(Some(title)) => Err(format!("unexpected title: \"{}\"", title)),
Ok(None) => Ok(()),
Err(e) => Err(format!("unexpected error: {}", e)),
}
}
#[test]
fn test_page_title_from_html_fails_with_non_utf8_characters() -> Result<(), String> {
match page_title_from_html(&vec![0, 159, 146, 150]) {
Ok(_) => Err(format!(
"unexpected success extracting the title from non UTF8 characters"
)),
Err(e)
if (*e).to_string()
== "Invalid UTF-8 sequence: invalid utf-8 sequence of 1 bytes from index 1" =>
{
Ok(())
}
Err(e) => Err(format!("unexpected error: {}", e)),
}
}
#[test]
#[ignore]
fn test_page_title_from_html_with_attributes() -> Result<(), String> {
unimplemented!()
}
#[test]
fn test_has_redirect() -> Result<(), String> {
let origin: Url = Url::parse("https://doc.rust-lang.org/std")
.map_err(|e| format!("could not parse origin URL: {}", e))?;
let destination: Url = Url::parse("https://doc.rust-lang.org/stable/std/")
.map_err(|e| format!("could not parse destination URL: {}", e))?;
let mut headers: Vec<Header<'_>> = Vec::new();
let redirect_header = Header {
name: "Location",
value: destination.as_str().as_bytes(),
};
headers.push(redirect_header);
let mut response = Response::new(&mut headers);
response.code = Some(301);
match has_redirect(&response, &origin) {
Ok(Some(u)) if u == destination => Ok(()),
Ok(Some(u)) => Err(format!("unexpected redirect location: \"{}\"", u)),
Ok(None) => Err(format!("could not find HTTP redirect location")),
Err(e) => Err(format!("could not parse HTTP Response: {}", e)),
}
}
#[test]
fn test_has_no_redirect() -> Result<(), String> {
let origin: Url = Url::parse("https://doc.rust-lang.org/std")
.map_err(|e| format!("could not parse origin URL: {}", e))?;
let mut headers: Vec<Header<'_>> = Vec::new();
let mut response = Response::new(&mut headers);
response.code = Some(200);
match has_redirect(&response, &origin) {
Ok(None) => Ok(()),
Ok(Some(u)) => Err(format!("unexpected redirect location: \"{}\"", u)),
Err(e) => Err(format!("could not parse HTTP Response: {}", e)),
}
}
#[test]
fn test_has_redirect_fails_without_status_code() -> Result<(), String> {
let origin: Url = Url::parse("https://doc.rust-lang.org/std")
.map_err(|e| format!("could not parse origin URL: {}", e))?;
let mut headers: Vec<Header<'_>> = Vec::new();
let mut response = Response::new(&mut headers);
response.code = None;
match has_redirect(&response, &origin) {
Ok(_) => Err(From::from("unexpected success in redirect detection")),
Err(e)
if (*e).to_string() == "HTTP response parsing error: could not find StatusCode" =>
{
Ok(())
}
Err(e) => Err(format!("unexpected error message: {}", e)),
}
}
#[test]
#[ignore]
fn test_has_redirect_fails_with_unparseable_redirect_location() -> Result<(), String> {
let origin: Url = Url::parse("https://doc.rust-lang.org/std")
.map_err(|e| format!("could not parse origin URL: {}", e))?;
let mut headers: Vec<Header<'_>> = Vec::new();
let redirect_header = Header {
name: "Location",
value: "unreachable".as_bytes(),
};
headers.push(redirect_header);
let mut response = Response::new(&mut headers);
response.code = Some(301);
match has_redirect(&response, &origin) {
Ok(l) => Err(format!("unexpected success in redirect detection: {:?}", l)),
Err(e)
if (*e).to_string() == "HTTP response parsing error: could not find StatusCode" =>
{
Ok(())
}
Err(e) => Err(format!("unexpected error message: {}", e)),
}
}
#[test]
fn test_has_redirect_fails_without_redirect_location() -> Result<(), String> {
let origin: Url = Url::parse("https://doc.rust-lang.org/std")
.map_err(|e| format!("could not parse origin URL: {}", e))?;
let mut headers: Vec<Header<'_>> = Vec::new();
let mut response = Response::new(&mut headers);
response.code = Some(301);
match has_redirect(&response, &origin) {
Ok(l) => Err(format!("unexpected success in redirect detection: {:?}", l)),
Err(e) if (*e).to_string() == "HTTP redirect without location: 301" => Ok(()),
Err(e) => Err(format!("unexpected error message: {}", e)),
}
}
#[test]
fn test_has_redirect_passes_with_a_relative_redirect() -> Result<(), String> {
let origin: Url = Url::parse("https://doc.rust-lang.org/stable")
.map_err(|e| format!("could not parse origin URL: {}", e))?;
let mut headers: Vec<Header<'_>> = Vec::new();
let redirect_header = Header {
name: "Location",
value: "/stable/std/".as_bytes(),
};
headers.push(redirect_header);
let mut response = Response::new(&mut headers);
response.code = Some(301);
match has_redirect(&response, &origin) {
Ok(None) => Err(From::from("missing redirect detection")),
Ok(Some(l)) if l.to_string() == "https://doc.rust-lang.org/stable/std/" => Ok(()),
Ok(Some(l)) => Err(format!("unexpected redirect location: {}", l.to_string())),
Err(e) => Err(format!("could not detect redirection: {}", e)),
}
}
#[cfg(feature = "live-tests")]
mod livetests {
use super::*;
use std::net::IpAddr;
use std::net::Ipv4Addr;
#[test]
fn test_page_content_fails_with_http_only_url() -> Result<(), String> {
let connector: TlsConnector = TlsConnector::default();
let url: Url = Url::parse("http://example.org")
.map_err(|e| format!("could not parse URL: {}", e))?;
let parts: Parts = Parts::try_from(&url)
.map_err(|e| format!("could not parse parts from URL: {}", e))?;
match task::block_on(async { page_content(&connector, &parts).await }) {
Ok(_) => Err(format!("unexpected OK connection: HTTP should fail")),
Err(e) if (*e).to_string() == "TLSStream error: received corrupt message" => Ok(()),
Err(e) => Err(format!("unexpected error message: {}", e)),
}
}
#[test]
fn test_page_content_fails_with_broken_address() -> Result<(), String> {
let connector: TlsConnector = TlsConnector::default();
let parts: Parts = Parts {
url: Url::parse("http://example.org").unwrap(),
host: Host::parse("example.org").unwrap(),
port: 443,
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
domain: String::new(),
path: String::new(),
query: String::new(),
fragment: String::new(),
};
match task::block_on(async { page_content(&connector, &parts).await }) {
Ok(_) => Err(format!("unexpected OK connection: HTTP should fail")),
Err(e)
if (*e).to_string() == "TCPStream error: Connection refused (os error 111)" =>
{
Ok(())
}
Err(e) => Err(format!("unexpected error message: {}", e)),
}
}
#[test]
fn test_page_content_from_url_with_jump_next() -> Result<(), String> {
let connector: TlsConnector = TlsConnector::default();
let url: Url = Url::parse("https://wikipedia.org")
.map_err(|e| format!("could not parse URL: {}", e))?;
let next: Url = Url::parse("https://www.wikipedia.org")
.map_err(|e| format!("could not parse URL: {}", e))?;
let parts: Parts = Parts::try_from(&url)
.map_err(|e| format!("could not parse parts from URL: {}", e))?;
match task::block_on(async { page_content(&connector, &parts).await }) {
Ok(Jump::Next(n)) if n == next => Ok(()),
Ok(Jump::Next(n)) => Err(format!("unexpected Jump::Next URL: {}", n)),
Ok(Jump::Landing(_)) => Err(format!("unexpected Jump::Landing")),
Err(e) => Err(format!("could not run live test: {}", e)),
}
}
#[test]
fn test_page_content_from_url_with_jump_landing() -> Result<(), String> {
let connector: TlsConnector = TlsConnector::default();
let url: Url = Url::parse("https://example.org")
.map_err(|e| format!("could not parse URL: {}", e))?;
let parts: Parts = Parts::try_from(&url)
.map_err(|e| format!("could not parse parts from URL: {}", e))?;
let http_ok_header: &str = "HTTP/1.0 200 OK";
match task::block_on(async { page_content(&connector, &parts).await }) {
Ok(Jump::Landing(content))
if from_utf8(&content[0..http_ok_header.len()]) == Ok(http_ok_header) =>
{
Ok(())
}
Ok(Jump::Landing(content)) => Err(format!(
"unexpected HTTP header: {:?}",
from_utf8(&content[0..http_ok_header.len()])
)),
Ok(Jump::Next(n)) => Err(format!("unexpected Jump::Next: {}", n)),
Err(e) => Err(format!("could not run live test: {}", e)),
}
}
mod parts {
use super::*;
#[test]
fn test_parts_try_from_options_fails_when_empty() -> Result<(), String> {
let options: Options = Options {
url: None,
clipboard: false,
host: None,
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if (*e).to_string()
== "use one and only one of --clipboard, --url or --host" =>
{
Ok(())
}
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_options_fails_with_both_url_and_host() -> Result<(), String> {
let options: Options = Options {
url: Some("url".to_string()),
clipboard: false,
host: Some("host".to_string()),
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if (*e).to_string()
== "use one and only one of --clipboard, --url or --host" =>
{
Ok(())
}
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_options_fails_with_both_clipboard_and_host() -> Result<(), String>
{
let options: Options = Options {
url: None,
clipboard: true,
host: Some("host".to_string()),
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if (*e).to_string()
== "use one and only one of --clipboard, --url or --host" =>
{
Ok(())
}
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_options_fails_with_both_clipboard_and_url() -> Result<(), String>
{
let options: Options = Options {
url: Some("url".to_string()),
clipboard: true,
host: None,
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if (*e).to_string()
== "use one and only one of --clipboard, --url or --host" =>
{
Ok(())
}
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_options_fails_with_both_clipboard_and_host_and_url()
-> Result<(), String> {
let options: Options = Options {
url: Some("url".to_string()),
clipboard: true,
host: Some("host".to_string()),
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if (*e).to_string()
== "use one and only one of --clipboard, --url or --host" =>
{
Ok(())
}
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_options_fails_with_non_existing_url() -> Result<(), String> {
let options: Options = Options {
url: Some("https://unresolvable.example.org".to_string()),
clipboard: false,
host: None,
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if e.to_string()
== "failed to lookup address information: Name or service not known" =>
{
Ok(())
}
Err(e) => Err(format!(
"unexpected error creating Parts from a URL: {}",
e.to_string()
)),
}
}
#[test]
fn test_parts_try_from_options_fails_with_non_supported_scheme() -> Result<(), String> {
let options: Options = Options {
url: Some("news://example.org".to_string()),
clipboard: false,
host: None,
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if e.to_string()
== "Only HTTP(S) standard ports are suported. news given" =>
{
Ok(())
}
Err(e) => Err(format!(
"unexpected error creating Parts from a URL: {}",
e.to_string()
)),
}
}
fn validate_parts_and_url(parts: Parts, url: Option<String>) -> Result<(), String> {
if let Some(url) = url {
if parts.url != Url::parse(&url).unwrap() {
return Err(format!(
"could not create Options with the correct URL, got: {}",
parts.url
));
}
}
if parts.host != Host::parse("example.org").unwrap() {
return Err(format!(
"could not create Options with the correct Host, got: {}",
parts.host
));
}
if parts.port != 443 {
return Err(format!(
"could not create Options with the correct port, got: {}",
parts.port
));
}
if parts.path != "/news/today.html".to_string() {
return Err(format!(
"could not create Options with the correct URL path, got: {}",
parts.path
));
}
if parts.query != "tag=top".to_string() {
return Err(format!(
"could not create Options with the correct query, got: {}",
parts.query
));
}
if parts.fragment != "headline".to_string() {
return Err(format!(
"could not create Options with the correct fragment, got: {}",
parts.fragment
));
}
if parts.domain != "example.org".to_string() {
return Err(format!(
"could not create Options with the correct domain, got: {}",
parts.domain
));
}
Ok(())
}
#[test]
fn test_parts_try_from_options_passes_with_existing_url() -> Result<(), String> {
let url: String =
"https://example.org/news/today.html?tag=top#headline".to_string();
let options: Options = Options {
url: Some(url.clone()),
clipboard: false,
host: None,
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(parts) => validate_parts_and_url(parts, options.url),
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_options_fails_with_http_url() -> Result<(), String> {
let url: String = "http://example.org".to_string();
let options: Options = Options {
url: Some(url.clone()),
clipboard: false,
host: None,
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e) if e.to_string() == "HTTP standard ports is not yet suported" => Ok(()),
Err(e) => Err(format!(
"unexpected error creating Parts from a URL: {}",
e.to_string()
)),
}
}
#[test]
fn test_parts_try_from_options_passes_ignoring_non_https_port_when_given()
-> Result<(), String> {
let url: String = "http://example.org".to_string();
let options: Options = Options {
url: Some(url.clone()),
clipboard: false,
host: None,
port: 8080u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Ok(()),
Err(e) => Err(format!(
"could not create Parts from a URL: {}",
e.to_string()
)),
}
}
#[test]
fn test_parts_try_from_options_passes_with_another_domain() -> Result<(), String> {
let url: String = "https://example.com".to_string();
let options: Options = Options {
url: Some(url.clone()),
clipboard: false,
host: None,
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: Some("unresolvable".to_string()),
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(_) => Ok(()),
Err(e) => Err(format!(
"could not create Parts from a URL: {}",
e.to_string()
)),
}
}
#[test]
fn test_parts_try_from_options_passes_with_existing_clipboard() -> Result<(), String> {
let url: String =
"https://example.org/news/today.html?tag=top#headline".to_string();
let clipboard = Clipboard::new().unwrap();
let atom_clipboard = clipboard.setter.atoms.clipboard;
let atom_utf8string = clipboard.setter.atoms.utf8_string;
clipboard
.store(atom_clipboard, atom_utf8string, url.as_bytes())
.map_err(|e| format!("could not store a value in the clipboard: {}", e))?;
let options: Options = Options {
url: None,
clipboard: true,
host: None,
port: 0u16,
scheme: "".to_string(),
path: "".to_string(),
query: None,
fragment: None,
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(parts) => validate_parts_and_url(parts, options.url),
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_options_passes_with_existing_host() -> Result<(), String> {
let options: Options = Options {
url: None,
clipboard: false,
host: Some("example.org".to_string()),
port: 0u16,
scheme: "https".to_string(),
path: "/news/today.html".to_string(),
query: Some("tag=top".to_string()),
fragment: Some("headline".to_string()),
domain: None,
cafile: None,
run: false,
};
match Parts::try_from(&options) {
Ok(parts) => validate_parts_and_url(parts, options.url),
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_str_passes_with_existing_url() -> Result<(), String> {
let url: &str = "https://example.org/news/today.html?tag=top#headline";
match Parts::try_from(url) {
Ok(parts) => validate_parts_and_url(parts, Some(url.to_string())),
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_str_fails_with_non_existing_url() -> Result<(), String> {
let url: &str = "https://unresolvable.example.org/news/today.html?tag=top#headline";
let parts: Result<Parts, Box<dyn Error>> = TryFrom::<&str>::try_from(url);
match parts {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if e.to_string()
== "failed to lookup address information: Name or service not known" =>
{
Ok(())
}
Err(e) => Err(format!(
"unexpected error creating Parts from a URL: {}",
e.to_string()
)),
}
}
#[test]
fn test_parts_try_from_str_fails_with_non_supported_scheme() -> Result<(), String> {
let url: &str = "news://example.org/news/today";
let parts: Result<Parts, Box<dyn Error>> = TryFrom::<&str>::try_from(url);
match parts {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if e.to_string()
== "Only HTTP(S) standard ports are suported. news given" =>
{
Ok(())
}
Err(e) => Err(format!(
"unexpected error creating Parts from a URL: {}",
e.to_string()
)),
}
}
#[test]
fn test_parts_try_from_url_passes_with_existing_url() -> Result<(), String> {
let url: &str = "https://example.org/news/today.html?tag=top#headline";
let url: Url = Url::parse(url).expect("cannot parse test URL");
let parts: Result<Parts, Box<dyn Error>> = TryFrom::<&Url>::try_from(&url);
match parts {
Ok(parts) => validate_parts_and_url(parts, Some(url.to_string())),
Err(e) => Err(format!("could not create Options: {}", e)),
}
}
#[test]
fn test_parts_try_from_url_fails_with_non_existing_url() -> Result<(), String> {
let url: &str = "https://unresolvable.example.org/news/today.html?tag=top#headline";
let url: Url = Url::parse(url).expect("cannot parse test URL");
let parts: Result<Parts, Box<dyn Error>> = TryFrom::<&Url>::try_from(&url);
match parts {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if e.to_string()
== "failed to lookup address information: Name or service not known" =>
{
Ok(())
}
Err(e) => Err(format!(
"unexpected error creating Parts from a URL: {}",
e.to_string()
)),
}
}
#[test]
fn test_parts_try_from_url_fails_with_non_supported_scheme() -> Result<(), String> {
let url: &str = "news://example.org/news/today";
let url: Url = Url::parse(url).expect("cannot parse test URL");
let parts: Result<Parts, Box<dyn Error>> = TryFrom::<&Url>::try_from(&url);
match parts {
Ok(_) => Err(format!("unexpected successful conversion into Parts")),
Err(e)
if e.to_string()
== "Only HTTP(S) standard ports are suported. news given" =>
{
Ok(())
}
Err(e) => Err(format!(
"unexpected error creating Parts from a URL: {}",
e.to_string()
)),
}
}
}
}
#[test]
fn test_clipboard_content_passes_trivially() -> Result<(), String> {
let expected = format!("{:?}", Instant::now());
let clipboard = Clipboard::new().unwrap();
let atom_clipboard = clipboard.setter.atoms.clipboard;
let atom_utf8string = clipboard.setter.atoms.utf8_string;
clipboard
.store(atom_clipboard, atom_utf8string, expected.as_bytes())
.map_err(|e| format!("could not store a value in the clipboard: {}", e))?;
match clipboard_content()
.map_err(|e| format!("could not get a value from the cliboard: {}", e))
{
Ok(content) if content == expected => Ok(()),
Ok(content) => Err(format!("unexpected paste content: {}", content)),
Err(e) => Err(format!("could not read the clipboard: {}", e)),
}
}
#[test]
fn test_parts_http_request_passes_without_url_query() -> Result<(), String> {
let parts: Parts = Parts::try_from("https://example.org/resource")
.map_err(|e| format!("could not create a Parts structure from a str: {}", e))?;
let expected: String = format!("GET /resource HTTP/1.0\r\nHost: example.org\r\n\r\n",);
match parts.http_request() {
req if req == expected => Ok(()),
req => Err(format!("unexpected HTTP request: {}", req)),
}
}
#[test]
fn test_parts_http_request_passes_with_url_query_and_fragment() -> Result<(), String> {
let parts: Parts =
Parts::try_from("https://example.org/resource.html?tag=news#headline")
.map_err(|e| format!("could not create a Parts structure from a str: {}", e))?;
let expected: String =
format!("GET /resource.html?tag=news#headline HTTP/1.0\r\nHost: example.org\r\n\r\n",);
match parts.http_request() {
req if req == expected => Ok(()),
req => Err(format!("unexpected HTTP request: {}", req)),
}
}
#[test]
fn test_parts_http_request_passes_with_url_query() -> Result<(), String> {
let parts: Parts = Parts::try_from("https://www.example.org/resource.html?tag=news")
.map_err(|e| format!("could not create a Parts structure from a str: {}", e))?;
let expected: String =
format!("GET /resource.html?tag=news HTTP/1.0\r\nHost: www.example.org\r\n\r\n",);
match parts.http_request() {
req if req == expected => Ok(()),
req => Err(format!("unexpected HTTP request: {}", req)),
}
}
#[test]
fn test_parts_http_request_passes_with_url_fragment() -> Result<(), String> {
let parts: Parts = Parts::try_from("https://www.example.org/resource.html#headline")
.map_err(|e| format!("could not create a Parts structure from a str: {}", e))?;
let expected: String =
format!("GET /resource.html#headline HTTP/1.0\r\nHost: www.example.org\r\n\r\n",);
match parts.http_request() {
req if req == expected => Ok(()),
req => Err(format!("unexpected HTTP request: {}", req)),
}
}
}