harn_modules/
package_imports.rs1use std::collections::{HashMap, HashSet};
2use std::path::{Component, Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::package_execution::{PackageExecutionError, PackageExecutionGuard};
7use crate::package_snapshot::PackageSnapshot;
8
9#[derive(Debug, Default, Deserialize)]
10struct PackageManifest {
11 #[serde(default)]
12 exports: HashMap<String, String>,
13}
14
15enum LocalResolution {
22 Resolved(PathBuf),
24 Rejected,
26 NotPackage,
28}
29
30fn resolve_local_import(current_file: &Path, import_path: &str) -> LocalResolution {
35 if let Some(module) = import_path
36 .strip_prefix("std/")
37 .or_else(|| (import_path == "observability").then_some("observability"))
38 {
39 return match super::stdlib::get_stdlib_source(module) {
40 Some(_) => LocalResolution::Resolved(super::stdlib::stdlib_virtual_path(module)),
41 None => LocalResolution::Rejected,
42 };
43 }
44
45 let base = current_file.parent().unwrap_or(Path::new("."));
46 let mut file_path = base.join(import_path);
47 if !file_path.exists() && file_path.extension().is_none() {
48 file_path.set_extension("harn");
49 }
50 if file_path.exists() {
51 return LocalResolution::Resolved(file_path);
52 }
53
54 LocalResolution::NotPackage
55}
56
57pub fn resolve_import_path(current_file: &Path, import_path: &str) -> Option<PathBuf> {
62 match resolve_local_import(current_file, import_path) {
63 LocalResolution::Resolved(path) => Some(path),
64 LocalResolution::Rejected => None,
65 LocalResolution::NotPackage => {
72 let snapshots = PackageSnapshot::acquire_nearest(current_file)
73 .ok()
74 .flatten()
75 .into_iter()
76 .collect::<Vec<_>>();
77 let resolved = resolve_package_import(current_file, import_path, &snapshots);
78 if resolved.is_some() {
79 for snapshot in snapshots {
80 snapshot.retain_for_process();
81 }
82 }
83 resolved
84 }
85 }
86}
87
88pub(crate) fn resolve_import_path_with_snapshots(
89 current_file: &Path,
90 import_path: &str,
91 package_snapshots: &[PackageSnapshot],
92) -> Option<PathBuf> {
93 match resolve_local_import(current_file, import_path) {
94 LocalResolution::Resolved(path) => Some(path),
95 LocalResolution::Rejected => None,
96 LocalResolution::NotPackage => {
97 resolve_package_import(current_file, import_path, package_snapshots)
98 }
99 }
100}
101
102pub fn resolve_import_path_with_snapshot(
103 current_file: &Path,
104 import_path: &str,
105 package_snapshot: &PackageSnapshot,
106) -> Option<PathBuf> {
107 match resolve_local_import(current_file, import_path) {
108 LocalResolution::Resolved(path) => Some(path),
109 LocalResolution::Rejected => None,
110 LocalResolution::NotPackage => {
114 resolve_from_packages_root(package_snapshot.packages_root(), import_path)
115 }
116 }
117}
118
119pub fn resolve_import_path_with_guard(
120 current_file: &Path,
121 import_path: &str,
122 guard: &PackageExecutionGuard,
123) -> Result<Option<PathBuf>, PackageExecutionError> {
124 guard.validate_import_path(current_file, import_path)?;
125 match resolve_local_import(current_file, import_path) {
126 LocalResolution::Resolved(path) => Ok(Some(path)),
127 LocalResolution::Rejected => Ok(None),
128 LocalResolution::NotPackage => resolve_from_packages_root_with_guard(
129 guard.snapshot().packages_root(),
130 import_path,
131 guard,
132 ),
133 }
134}
135
136pub(crate) fn acquire_package_snapshots(files: &[PathBuf]) -> Vec<PackageSnapshot> {
144 let mut walked_roots = HashSet::new();
145 let mut canonical_roots = HashSet::new();
146 let mut snapshots = Vec::new();
147 for file in files {
148 let Some(root) = PackageSnapshot::nearest_project_root(file) else {
150 continue;
151 };
152 if !walked_roots.insert(root.clone()) {
153 continue;
154 }
155 let Ok(Some(snapshot)) = PackageSnapshot::acquire(&root) else {
157 continue;
158 };
159 if canonical_roots.insert(snapshot.project_root().to_path_buf()) {
164 snapshots.push(snapshot);
165 }
166 }
167 snapshots
168}
169
170fn resolve_package_import(
171 current_file: &Path,
172 import_path: &str,
173 package_snapshots: &[PackageSnapshot],
174) -> Option<PathBuf> {
175 let current_file = canonicalize_with_existing_parent(current_file);
176 package_snapshots
177 .iter()
178 .filter(|snapshot| current_file.starts_with(snapshot.project_root()))
179 .max_by_key(|snapshot| snapshot.project_root().components().count())
180 .and_then(|snapshot| resolve_from_packages_root(snapshot.packages_root(), import_path))
181}
182
183fn canonicalize_with_existing_parent(path: &Path) -> PathBuf {
184 path.canonicalize().unwrap_or_else(|_| {
185 path.parent()
186 .and_then(|parent| parent.canonicalize().ok())
187 .and_then(|parent| path.file_name().map(|name| parent.join(name)))
188 .unwrap_or_else(|| path.to_path_buf())
189 })
190}
191
192fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
193 let safe_import_path = safe_package_relative_path(import_path)?;
194 let package_name = package_name_from_relative_path(&safe_import_path)?;
195 let package_root = packages_root.join(package_name);
196
197 let direct_path = packages_root.join(&safe_import_path);
198 if let Some(path) = finalize_package_target(&package_root, &direct_path) {
199 return Some(path);
200 }
201
202 let export_name = export_name_from_relative_path(&safe_import_path)?;
203 let manifest = read_package_manifest(&package_root.join("harn.toml"))?;
204 let safe_export_path = safe_package_relative_path(manifest.exports.get(export_name)?)?;
205 finalize_package_target(&package_root, &package_root.join(safe_export_path))
206}
207
208fn resolve_from_packages_root_with_guard(
209 packages_root: &Path,
210 import_path: &str,
211 guard: &PackageExecutionGuard,
212) -> Result<Option<PathBuf>, PackageExecutionError> {
213 let Some(safe_import_path) = safe_package_relative_path(import_path) else {
214 return Ok(None);
215 };
216 let Some(package_name) = package_name_from_relative_path(&safe_import_path) else {
217 return Ok(None);
218 };
219 let package_root = packages_root.join(package_name);
220 let direct_path = packages_root.join(&safe_import_path);
221 if let Some(path) = finalize_package_target(&package_root, &direct_path) {
222 return Ok(Some(path));
223 }
224
225 let Some(export_name) = export_name_from_relative_path(&safe_import_path) else {
226 return Ok(None);
227 };
228 let manifest_path = package_root.join("harn.toml");
229 let bytes = guard.verify_entry_source(&manifest_path)?;
230 let source = std::str::from_utf8(&bytes).map_err(|error| {
231 PackageExecutionError::Invalid(format!(
232 "package manifest {} is not valid UTF-8: {error}",
233 manifest_path.display()
234 ))
235 })?;
236 let manifest = toml::from_str::<PackageManifest>(source).map_err(|error| {
237 PackageExecutionError::Invalid(format!(
238 "failed to parse package exports from {}: {error}",
239 manifest_path.display()
240 ))
241 })?;
242 let Some(export_path) = manifest.exports.get(export_name) else {
243 return Ok(None);
244 };
245 let Some(safe_export_path) = safe_package_relative_path(export_path) else {
246 return Ok(None);
247 };
248 Ok(finalize_package_target(
249 &package_root,
250 &package_root.join(safe_export_path),
251 ))
252}
253
254fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
255 let content = std::fs::read_to_string(path).ok()?;
256 toml::from_str(&content).ok()
257}
258
259fn safe_package_relative_path(raw: &str) -> Option<PathBuf> {
260 if raw.is_empty() || raw.contains('\\') {
261 return None;
262 }
263 let mut out = PathBuf::new();
264 let mut saw_component = false;
265 for component in Path::new(raw).components() {
266 match component {
267 Component::Normal(part) => {
268 saw_component = true;
269 out.push(part);
270 }
271 Component::CurDir => {}
272 Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
273 }
274 }
275 saw_component.then_some(out)
276}
277
278fn package_name_from_relative_path(path: &Path) -> Option<&str> {
279 match path.components().next()? {
280 Component::Normal(name) => name.to_str(),
281 _ => None,
282 }
283}
284
285fn export_name_from_relative_path(path: &Path) -> Option<&str> {
286 let mut components = path.components();
287 components.next()?;
288 let rest = components.as_path();
289 if rest.as_os_str().is_empty() {
290 None
291 } else {
292 rest.to_str()
293 }
294}
295
296fn target_within_package_root(package_root: &Path, path: PathBuf) -> Option<PathBuf> {
297 let root = package_root.canonicalize().ok()?;
298 let canonical = path.canonicalize().ok()?;
299 (canonical == root || canonical.starts_with(&root)).then_some(path)
300}
301
302fn finalize_package_target(package_root: &Path, path: &Path) -> Option<PathBuf> {
303 if path.is_dir() {
304 let lib = path.join("lib.harn");
305 return if lib.exists() {
306 target_within_package_root(package_root, lib)
307 } else {
308 target_within_package_root(package_root, path.to_path_buf())
309 };
310 }
311 if path.exists() {
312 return target_within_package_root(package_root, path.to_path_buf());
313 }
314 if path.extension().is_none() {
315 let mut with_extension = path.to_path_buf();
316 with_extension.set_extension("harn");
317 if with_extension.exists() {
318 return target_within_package_root(package_root, with_extension);
319 }
320 }
321 None
322}