1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone)]
4pub struct ModuleContext {
5 pub module_id: String,
6 pub base_dir: Option<PathBuf>,
7}
8
9#[derive(Debug, Clone)]
10pub struct ResolvedModule {
11 pub module_id: String,
12 pub source: String,
13 pub base_dir: Option<PathBuf>,
14}
15
16#[derive(Debug, Clone)]
17pub enum ModuleResolveError {
18 ImportDisabled,
19 InvalidSpecifier(String),
20 NoBaseDir(String),
21 NotFound(String),
22 AccessDenied(String),
23 IoError(String),
24}
25
26impl std::fmt::Display for ModuleResolveError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 ModuleResolveError::ImportDisabled => write!(f, "Import is disabled"),
30 ModuleResolveError::InvalidSpecifier(s) => write!(f, "Invalid module specifier: {s}"),
31 ModuleResolveError::NoBaseDir(s) => {
32 write!(f, "No base directory to resolve specifier: {s}")
33 }
34 ModuleResolveError::NotFound(s) => write!(f, "Module not found: {s}"),
35 ModuleResolveError::AccessDenied(s) => write!(f, "Module access denied: {s}"),
36 ModuleResolveError::IoError(s) => write!(f, "Module IO error: {s}"),
37 }
38 }
39}
40
41impl std::error::Error for ModuleResolveError {}
42
43pub trait ModuleResolver {
44 fn resolve(
45 &self,
46 specifier: &str,
47 from: Option<&ModuleContext>,
48 ) -> Result<ResolvedModule, ModuleResolveError>;
49}
50
51#[derive(Default, Debug, Clone)]
52pub struct DisabledModuleResolver;
53
54impl ModuleResolver for DisabledModuleResolver {
55 fn resolve(
56 &self,
57 _specifier: &str,
58 _from: Option<&ModuleContext>,
59 ) -> Result<ResolvedModule, ModuleResolveError> {
60 Err(ModuleResolveError::ImportDisabled)
61 }
62}
63
64#[derive(Debug, Clone, Default)]
65pub struct FileSystemModuleResolver {
66 pub root_dir: Option<PathBuf>,
68 pub allow_absolute: bool,
70}
71
72impl FileSystemModuleResolver {
73 fn normalize_specifier(specifier: &str) -> Result<&str, ModuleResolveError> {
74 if specifier.contains("://") {
75 return Err(ModuleResolveError::InvalidSpecifier(specifier.to_string()));
76 }
77 Ok(specifier)
78 }
79
80 fn with_aether_extension(path: &Path) -> PathBuf {
81 if path.extension().is_some() {
82 path.to_path_buf()
83 } else {
84 let mut p = path.to_path_buf();
85 p.set_extension("aether");
86 p
87 }
88 }
89
90 fn ensure_under_root(&self, path: &Path) -> Result<(), ModuleResolveError> {
91 if let Some(root) = &self.root_dir {
92 let root = match root.canonicalize() {
93 Ok(p) => p,
94 Err(e) => return Err(ModuleResolveError::IoError(e.to_string())),
95 };
96 let canon = match path.canonicalize() {
97 Ok(p) => p,
98 Err(e) => return Err(ModuleResolveError::IoError(e.to_string())),
99 };
100 if !canon.starts_with(&root) {
101 return Err(ModuleResolveError::AccessDenied(
102 canon.display().to_string(),
103 ));
104 }
105 }
106 Ok(())
107 }
108
109 fn resolve_path(
110 &self,
111 specifier: &str,
112 from: Option<&ModuleContext>,
113 ) -> Result<PathBuf, ModuleResolveError> {
114 let specifier = Self::normalize_specifier(specifier)?;
115
116 let raw = PathBuf::from(specifier);
117
118 if raw.is_absolute() {
119 if !self.allow_absolute {
120 return Err(ModuleResolveError::AccessDenied(specifier.to_string()));
121 }
122 return Ok(Self::with_aether_extension(&raw));
123 }
124
125 let base_dir = from
126 .and_then(|c| c.base_dir.clone())
127 .ok_or_else(|| ModuleResolveError::NoBaseDir(specifier.to_string()))?;
128
129 Ok(Self::with_aether_extension(&base_dir.join(raw)))
130 }
131}
132
133impl ModuleResolver for FileSystemModuleResolver {
134 fn resolve(
135 &self,
136 specifier: &str,
137 from: Option<&ModuleContext>,
138 ) -> Result<ResolvedModule, ModuleResolveError> {
139 let path = self.resolve_path(specifier, from)?;
140
141 if !path.exists() {
142 return Err(ModuleResolveError::NotFound(path.display().to_string()));
143 }
144
145 self.ensure_under_root(&path)?;
146
147 let canon = path
148 .canonicalize()
149 .map_err(|e| ModuleResolveError::IoError(e.to_string()))?;
150
151 let source = std::fs::read_to_string(&canon)
152 .map_err(|e| ModuleResolveError::IoError(e.to_string()))?;
153
154 let base_dir = canon.parent().map(|p| p.to_path_buf());
155
156 Ok(ResolvedModule {
157 module_id: canon.display().to_string(),
158 source,
159 base_dir,
160 })
161 }
162}