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
}