aether/
module_system.rs

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    /// Optional root directory; when set, resolved paths must be under this root.
67    pub root_dir: Option<PathBuf>,
68    /// Whether to allow absolute paths.
69    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}