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 resolve_package_import(current_file, import_path, &snapshots)
78 }
79 }
80}
81
82pub(crate) fn resolve_import_path_with_snapshots(
83 current_file: &Path,
84 import_path: &str,
85 package_snapshots: &[PackageSnapshot],
86) -> Option<PathBuf> {
87 match resolve_local_import(current_file, import_path) {
88 LocalResolution::Resolved(path) => Some(path),
89 LocalResolution::Rejected => None,
90 LocalResolution::NotPackage => {
91 resolve_package_import(current_file, import_path, package_snapshots)
92 }
93 }
94}
95
96pub fn resolve_import_path_with_snapshot(
97 current_file: &Path,
98 import_path: &str,
99 package_snapshot: &PackageSnapshot,
100) -> Option<PathBuf> {
101 match resolve_local_import(current_file, import_path) {
102 LocalResolution::Resolved(path) => Some(path),
103 LocalResolution::Rejected => None,
104 LocalResolution::NotPackage => {
108 resolve_from_packages_root(package_snapshot.packages_root(), import_path)
109 }
110 }
111}
112
113pub fn resolve_import_path_with_guard(
114 current_file: &Path,
115 import_path: &str,
116 guard: &PackageExecutionGuard,
117) -> Result<Option<PathBuf>, PackageExecutionError> {
118 guard.validate_import_path(current_file, import_path)?;
119 match resolve_local_import(current_file, import_path) {
120 LocalResolution::Resolved(path) => Ok(Some(path)),
121 LocalResolution::Rejected => Ok(None),
122 LocalResolution::NotPackage => resolve_from_packages_root_with_guard(
123 guard.snapshot().packages_root(),
124 import_path,
125 guard,
126 ),
127 }
128}
129
130pub(crate) fn acquire_package_snapshots(files: &[PathBuf]) -> Vec<PackageSnapshot> {
138 let mut walked_roots = HashSet::new();
139 let mut canonical_roots = HashSet::new();
140 let mut snapshots = Vec::new();
141 for file in files {
142 let Some(root) = PackageSnapshot::nearest_project_root(file) else {
144 continue;
145 };
146 if !walked_roots.insert(root.clone()) {
147 continue;
148 }
149 let Ok(Some(snapshot)) = PackageSnapshot::acquire(&root) else {
151 continue;
152 };
153 if canonical_roots.insert(snapshot.project_root().to_path_buf()) {
158 snapshots.push(snapshot);
159 }
160 }
161 snapshots
162}
163
164fn resolve_package_import(
165 current_file: &Path,
166 import_path: &str,
167 package_snapshots: &[PackageSnapshot],
168) -> Option<PathBuf> {
169 let current_file = canonicalize_with_existing_parent(current_file);
170 package_snapshots
171 .iter()
172 .filter(|snapshot| current_file.starts_with(snapshot.project_root()))
173 .max_by_key(|snapshot| snapshot.project_root().components().count())
174 .and_then(|snapshot| resolve_from_packages_root(snapshot.packages_root(), import_path))
175}
176
177fn canonicalize_with_existing_parent(path: &Path) -> PathBuf {
178 path.canonicalize().unwrap_or_else(|_| {
179 path.parent()
180 .and_then(|parent| parent.canonicalize().ok())
181 .and_then(|parent| path.file_name().map(|name| parent.join(name)))
182 .unwrap_or_else(|| path.to_path_buf())
183 })
184}
185
186fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
187 let safe_import_path = safe_package_relative_path(import_path)?;
188 let package_name = package_name_from_relative_path(&safe_import_path)?;
189 let package_root = packages_root.join(package_name);
190
191 let direct_path = packages_root.join(&safe_import_path);
192 if let Some(path) = finalize_package_target(&package_root, &direct_path) {
193 return Some(path);
194 }
195
196 let export_name = export_name_from_relative_path(&safe_import_path)?;
197 let manifest = read_package_manifest(&package_root.join("harn.toml"))?;
198 let safe_export_path = safe_package_relative_path(manifest.exports.get(export_name)?)?;
199 finalize_package_target(&package_root, &package_root.join(safe_export_path))
200}
201
202fn resolve_from_packages_root_with_guard(
203 packages_root: &Path,
204 import_path: &str,
205 guard: &PackageExecutionGuard,
206) -> Result<Option<PathBuf>, PackageExecutionError> {
207 let Some(safe_import_path) = safe_package_relative_path(import_path) else {
208 return Ok(None);
209 };
210 let Some(package_name) = package_name_from_relative_path(&safe_import_path) else {
211 return Ok(None);
212 };
213 let package_root = packages_root.join(package_name);
214 let direct_path = packages_root.join(&safe_import_path);
215 if let Some(path) = finalize_package_target(&package_root, &direct_path) {
216 return Ok(Some(path));
217 }
218
219 let Some(export_name) = export_name_from_relative_path(&safe_import_path) else {
220 return Ok(None);
221 };
222 let manifest_path = package_root.join("harn.toml");
223 let bytes = guard.verify_entry_source(&manifest_path)?;
224 let source = std::str::from_utf8(&bytes).map_err(|error| {
225 PackageExecutionError::Invalid(format!(
226 "package manifest {} is not valid UTF-8: {error}",
227 manifest_path.display()
228 ))
229 })?;
230 let manifest = toml::from_str::<PackageManifest>(source).map_err(|error| {
231 PackageExecutionError::Invalid(format!(
232 "failed to parse package exports from {}: {error}",
233 manifest_path.display()
234 ))
235 })?;
236 let Some(export_path) = manifest.exports.get(export_name) else {
237 return Ok(None);
238 };
239 let Some(safe_export_path) = safe_package_relative_path(export_path) else {
240 return Ok(None);
241 };
242 Ok(finalize_package_target(
243 &package_root,
244 &package_root.join(safe_export_path),
245 ))
246}
247
248fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
249 let content = std::fs::read_to_string(path).ok()?;
250 toml::from_str(&content).ok()
251}
252
253fn safe_package_relative_path(raw: &str) -> Option<PathBuf> {
254 if raw.is_empty() || raw.contains('\\') {
255 return None;
256 }
257 let mut out = PathBuf::new();
258 let mut saw_component = false;
259 for component in Path::new(raw).components() {
260 match component {
261 Component::Normal(part) => {
262 saw_component = true;
263 out.push(part);
264 }
265 Component::CurDir => {}
266 Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
267 }
268 }
269 saw_component.then_some(out)
270}
271
272fn package_name_from_relative_path(path: &Path) -> Option<&str> {
273 match path.components().next()? {
274 Component::Normal(name) => name.to_str(),
275 _ => None,
276 }
277}
278
279fn export_name_from_relative_path(path: &Path) -> Option<&str> {
280 let mut components = path.components();
281 components.next()?;
282 let rest = components.as_path();
283 if rest.as_os_str().is_empty() {
284 None
285 } else {
286 rest.to_str()
287 }
288}
289
290fn target_within_package_root(package_root: &Path, path: PathBuf) -> Option<PathBuf> {
291 let root = package_root.canonicalize().ok()?;
292 let canonical = path.canonicalize().ok()?;
293 (canonical == root || canonical.starts_with(&root)).then_some(path)
294}
295
296fn finalize_package_target(package_root: &Path, path: &Path) -> Option<PathBuf> {
297 if path.is_dir() {
298 let lib = path.join("lib.harn");
299 return if lib.exists() {
300 target_within_package_root(package_root, lib)
301 } else {
302 target_within_package_root(package_root, path.to_path_buf())
303 };
304 }
305 if path.exists() {
306 return target_within_package_root(package_root, path.to_path_buf());
307 }
308 if path.extension().is_none() {
309 let mut with_extension = path.to_path_buf();
310 with_extension.set_extension("harn");
311 if with_extension.exists() {
312 return target_within_package_root(package_root, with_extension);
313 }
314 }
315 None
316}