Skip to main content

perl_module_resolution_uri/
lib.rs

1//! Deterministic Perl module URI resolution helpers.
2//!
3//! This microcrate extracts the URI-first, timeout-bounded resolution policy from
4//! the broader `perl-module-resolution` crate so it can evolve independently.
5
6#![deny(unsafe_code)]
7#![warn(rust_2018_idioms)]
8#![warn(missing_docs)]
9#![warn(clippy::all)]
10
11use perl_module_path::module_name_to_path;
12use perl_path_security::validate_workspace_path;
13use perl_workspace_folder::workspace_folder_to_path;
14use std::path::PathBuf;
15use std::time::{Duration, Instant};
16use url::Url;
17
18/// Outcome of a module name to URI resolution attempt.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum ModuleUriResolution {
21    /// A matching module URI was found.
22    Resolved(String),
23    /// No matching module was found.
24    NotFound,
25    /// Resolution stopped because the timeout budget was exhausted.
26    TimedOut,
27}
28
29/// Resolve a module name to a `file://` URI using deterministic precedence.
30///
31/// Search order:
32/// 1. Open document URIs (`ends_with` match on relative module path)
33/// 2. Workspace folders + `include_paths` (path-safe filesystem checks)
34/// 3. System `@INC` paths (when `use_system_inc` is true)
35///
36/// The search observes `timeout` and returns [`ModuleUriResolution::TimedOut`] if
37/// the budget is exhausted.
38#[must_use]
39pub fn resolve_module_uri(
40    module_name: &str,
41    open_document_uris: &[String],
42    workspace_folders: &[String],
43    include_paths: &[String],
44    use_system_inc: bool,
45    system_inc: &[PathBuf],
46    timeout: Duration,
47) -> ModuleUriResolution {
48    let start_time = Instant::now();
49    let relative_path = module_name_to_path(module_name);
50
51    for uri in open_document_uris {
52        if uri.ends_with(&relative_path) {
53            return ModuleUriResolution::Resolved(uri.clone());
54        }
55    }
56
57    for workspace_folder in workspace_folders {
58        if start_time.elapsed() > timeout {
59            return ModuleUriResolution::TimedOut;
60        }
61
62        let workspace_path = workspace_folder_to_path(workspace_folder);
63
64        for include_path in include_paths {
65            if start_time.elapsed() > timeout {
66                return ModuleUriResolution::TimedOut;
67            }
68
69            let full_path = if include_path == "." {
70                workspace_path.join(&relative_path)
71            } else {
72                workspace_path.join(include_path).join(&relative_path)
73            };
74
75            let full_path = match validate_workspace_path(&full_path, &workspace_path) {
76                Ok(path) => path,
77                Err(_) => continue,
78            };
79
80            if full_path.is_file()
81                && let Ok(url) = Url::from_file_path(&full_path)
82            {
83                return ModuleUriResolution::Resolved(url.to_string());
84            }
85        }
86    }
87
88    if use_system_inc {
89        for inc_path in system_inc {
90            if start_time.elapsed() > timeout {
91                return ModuleUriResolution::TimedOut;
92            }
93
94            let full_path = inc_path.join(&relative_path);
95            if full_path.is_file()
96                && let Ok(url) = Url::from_file_path(&full_path)
97            {
98                return ModuleUriResolution::Resolved(url.to_string());
99            }
100        }
101    }
102
103    ModuleUriResolution::NotFound
104}