use super::{EvalContext, file_atoms, read};
use crate::action::AtomicAction;
use crate::host::{self, web};
use crate::shell::Unparsed;
pub(super) fn named(
url: &str,
what: &str,
ctx: &EvalContext,
atoms: &mut Vec<AtomicAction>,
) -> Option<Unparsed> {
match web::target(url) {
web::Target::Hosts(hosts) => {
atoms.extend(hosts.into_iter().map(|host| AtomicAction::Net { host }));
None
}
web::Target::File { path, host } => {
atoms.extend(host.map(|host| AtomicAction::Net { host }));
match path {
Some(path) => {
atoms.extend(file_atoms(&path, ctx, read));
None
}
None => Some(Unparsed {
reason: format!("file URL without an absolute path in {what} argument `{url}`"),
needs_review: true,
}),
}
}
web::Target::Blank => None,
web::Target::Local(scheme) => Some(Unparsed {
reason: format!(
"a `{scheme}:` URL in {what} argument names no network address: it runs code \
or shows content or browser pages no rule can name"
),
needs_review: true,
}),
web::Target::Unreadable => Some(Unparsed {
reason: format!("no host in {what} argument `{url}`"),
needs_review: false,
}),
}
}
pub(super) fn opened(
url: &str,
as_fetch: bool,
ctx: &EvalContext,
atoms: &mut Vec<AtomicAction>,
) -> Option<Unparsed> {
let url = web::with_scheme(url);
let unparsed = match web::page(&url).filter(|_| as_fetch) {
Some(hosts) => {
atoms.extend(hosts.into_iter().map(|host| AtomicAction::Fetch { host }));
None
}
None => named(&url, "mcp", ctx, atoms),
};
atoms.extend(web::embedded_hosts(&url).into_iter().map(|host| {
if as_fetch && host::fetchable(&host) {
AtomicAction::Fetch { host }
} else {
AtomicAction::Net { host }
}
}));
unparsed
}
#[cfg(test)]
mod tests {
use super::{named, opened};
use crate::action::AtomicAction;
use crate::engine::EvalContext;
fn ctx() -> EvalContext {
EvalContext {
home: "/Users/me".into(),
project: Some("/p".into()),
real_home: None,
real_project: None,
moved_dirs: Vec::new(),
cwd: "/p".into(),
case_insensitive_paths: false,
}
}
fn fetch(host: &str) -> AtomicAction {
AtomicAction::Fetch { host: host.into() }
}
fn net(host: &str) -> AtomicAction {
AtomicAction::Net { host: host.into() }
}
fn open(url: &str, as_fetch: bool) -> (Vec<AtomicAction>, Option<bool>) {
let mut atoms = Vec::new();
let why = opened(url, as_fetch, &ctx(), &mut atoms);
(atoms, why.map(|u| u.needs_review))
}
#[test]
fn a_plain_page_is_a_fetch_and_anything_else_is_not() {
assert_eq!(
open("https://docs.rs/serde", true),
(vec![fetch("docs.rs")], None)
);
assert_eq!(open("docs.rs/serde", true), (vec![fetch("docs.rs")], None));
assert_eq!(
open("https://docs.rs/serde", false),
(vec![net("docs.rs")], None)
);
assert_eq!(
open("http://localhost:3000/", true),
(vec![net("localhost")], None)
);
assert_eq!(
open("https://github.com@evil.com/", true),
(vec![net("evil.com")], None)
);
assert_eq!(
open("https://docs.rs/r?next=http://169.254.169.254/", true),
(vec![fetch("docs.rs"), net("169.254.169.254")], None)
);
assert_eq!(
open("https://docs.rs/r?next=https%3A%2F%2Fevil.com", true),
(vec![fetch("docs.rs"), fetch("evil.com")], None)
);
assert_eq!(open("about:blank", true), (vec![], None));
assert_eq!(open("javascript:alert(1)", true), (vec![], Some(true)));
let (atoms, why) = open("file:///Users/me/.ssh/id_rsa", true);
assert_eq!(
atoms,
[AtomicAction::FsRead {
path: "/Users/me/.ssh/id_rsa".into()
}]
);
assert_eq!(why, None);
}
#[test]
fn a_named_url_is_judged_by_every_host_a_client_may_reach() {
let mut atoms = Vec::new();
assert!(named("https://evil.com\\@github.com/", "mcp", &ctx(), &mut atoms).is_none());
assert_eq!(atoms, [net("github.com"), net("evil.com")]);
let mut atoms = Vec::new();
let why = named("file:relative", "mcp", &ctx(), &mut atoms).unwrap();
assert!(why.needs_review, "{}", why.reason);
let mut atoms = Vec::new();
assert!(named("file://evil.com/share/x", "mcp", &ctx(), &mut atoms).is_none());
assert_eq!(atoms[0], net("evil.com"));
}
}