use super::*;
pub(super) fn local_module_path(current_path: &Path, uri: &str) -> Option<PathBuf> {
if uri.starts_with("file:") {
return file_uri_path(uri).ok();
}
if uri.contains("://") {
return None;
}
if let Some(base) = current_path.to_str()
&& (base.starts_with("http://") || base.starts_with("https://"))
{
return None;
}
Some(current_path.parent().unwrap_or(Path::new(".")).join(uri))
}
pub(super) fn resolve_remote_relative(current_path: &Path, uri: &str) -> Option<String> {
if uri.contains("://") || uri.starts_with("file:") || uri.starts_with("pkl:") {
return None;
}
let base = current_path.to_str()?;
if !(base.starts_with("http://") || base.starts_with("https://")) {
return None;
}
resolve_http_relative(base, uri)
}
pub(super) fn resolve_http_relative(base: &str, uri: &str) -> Option<String> {
url::Url::parse(base)
.ok()?
.join(uri)
.ok()
.map(|url| url.to_string())
}
pub(super) fn canonical_remote_module_identity(uri: &str) -> String {
url::Url::parse(uri)
.map(|url| url.to_string())
.unwrap_or_else(|_| uri.to_string())
}
pub(crate) fn parse_triple_dot_path(uri: &str) -> Result<Option<&str>> {
let Some(rest) = uri.strip_prefix("...") else {
return Ok(None);
};
if rest.is_empty() {
return Ok(Some(""));
}
match rest.strip_prefix('/') {
Some(path) if !path.is_empty() => Ok(Some(path)),
_ => Err(Error::Eval(format!(
"Module URI `{uri}` has invalid syntax: expected `...` or `.../path/to/my_module.pkl`"
))),
}
}
pub(crate) fn resolve_triple_dot(
current_path: &Path,
path: &str,
root: Option<&Path>,
mut exists: impl FnMut(&Path) -> Result<bool>,
) -> Result<Option<PathBuf>> {
let current = normalize_lexical_path(current_path);
let file_name;
let path = if path.is_empty() {
file_name = current
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
file_name.as_str()
} else {
path
};
let root = root.or_else(|| current.ancestors().last());
let mut candidates = current
.parent()
.into_iter()
.flat_map(Path::ancestors)
.skip(1)
.take_while(|dir| root.is_none_or(|root| dir.starts_with(root)))
.collect::<Vec<_>>();
if let Some(root) = root
&& candidates.last() != Some(&root)
{
candidates.push(root);
}
for dir in candidates {
let candidate = normalize_lexical_path(&dir.join(path));
if candidate != current && exists(&candidate)? {
return Ok(Some(candidate));
}
}
Ok(None)
}
fn normalize_lexical_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
match normalized.components().next_back() {
Some(std::path::Component::Normal(_)) => {
normalized.pop();
}
Some(std::path::Component::RootDir | std::path::Component::Prefix(_)) => {}
Some(std::path::Component::ParentDir) | None => {
normalized.push(component.as_os_str());
}
Some(std::path::Component::CurDir) => unreachable!("paths are normalized"),
}
}
_ => normalized.push(component.as_os_str()),
}
}
normalized
}
#[cfg(test)]
mod remote_relative_tests {
use super::{
Path, canonical_remote_module_identity, resolve_http_relative, resolve_remote_relative,
};
#[test]
fn http_relative_resolves_against_base_directory() {
assert_eq!(
resolve_http_relative("https://example.com/cfg/Main.pkl", "../Lib.pkl").as_deref(),
Some("https://example.com/Lib.pkl")
);
}
#[test]
fn http_relative_ignores_base_query_and_fragment() {
assert_eq!(
resolve_http_relative(
"https://example.com/cfg/Main.pkl?version=2#part",
"../Lib.pkl"
)
.as_deref(),
Some("https://example.com/Lib.pkl")
);
}
#[test]
fn http_relative_keeps_absolute_path_references_absolute() {
assert_eq!(
resolve_http_relative("https://example.com/cfg/Main.pkl", "/shared/Lib.pkl").as_deref(),
Some("https://example.com/shared/Lib.pkl")
);
}
#[test]
fn http_relative_clamps_above_root_segments() {
assert_eq!(
resolve_http_relative("https://example.com/Main.pkl", "../../Lib.pkl").as_deref(),
Some("https://example.com/Lib.pkl")
);
}
#[test]
fn http_relative_resolves_query_only_references() {
assert_eq!(
resolve_http_relative("https://example.com/cfg/Main.pkl", "?v=2").as_deref(),
Some("https://example.com/cfg/Main.pkl?v=2")
);
}
#[test]
fn http_relative_resolves_fragment_only_references() {
assert_eq!(
resolve_http_relative("https://example.com/cfg/Main.pkl", "#frag").as_deref(),
Some("https://example.com/cfg/Main.pkl#frag")
);
}
#[test]
fn http_relative_resolves_network_path_references() {
assert_eq!(
resolve_http_relative("https://example.com/cfg/Main.pkl", "//cdn.example/Lib.pkl")
.as_deref(),
Some("https://cdn.example/Lib.pkl")
);
}
#[test]
fn http_relative_does_not_rewrite_file_uris() {
let base = Path::new("https://example.com/cfg/Main.pkl");
assert_eq!(resolve_remote_relative(base, "file:/tmp/Lib.pkl"), None);
assert_eq!(resolve_remote_relative(base, "file:///tmp/Lib.pkl"), None);
}
#[test]
fn remote_module_identity_normalizes_dot_segments() {
let absolute =
canonical_remote_module_identity("https://example.com/cfg/../shared/Lib.pkl");
let relative =
resolve_http_relative("https://example.com/cfg/Main.pkl", "../shared/Lib.pkl").unwrap();
assert_eq!(absolute, relative);
assert_eq!(absolute, "https://example.com/shared/Lib.pkl");
}
}