1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6use crate::ast::{FieldValue, PgnPacket};
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
9pub enum ResolutionStatus {
10 Resolved,
11 Missing,
12 Unresolved,
13 Forbidden,
14}
15
16#[derive(Debug, Clone, Serialize)]
17pub struct ResolvedRef {
18 pub original: String,
19 pub namespace: String,
20 pub ref_id: String,
21 pub resolved_path: Option<std::path::PathBuf>,
22 pub confidence: f32,
23 pub required: bool,
24 pub status: ResolutionStatus,
25}
26
27#[derive(Debug, Clone, Deserialize)]
28pub struct ReferenceAliases {
29 pub aliases: BTreeMap<String, String>,
30 pub common: BTreeMap<String, String>,
31}
32
33#[derive(Debug)]
34pub struct ResolverContext {
35 pub host_root: PathBuf,
36 pub aliases: ReferenceAliases,
37 pub required_inputs: Vec<String>,
38}
39
40pub fn parse_ref(reference: &str) -> Option<(String, String)> {
41 let trimmed = reference.trim();
42 if trimmed.is_empty() {
43 return None;
44 }
45 if let Some(idx) = trimmed.find(':') {
46 let namespace = trimmed[..idx].trim().to_string();
47 let ref_id = trimmed[idx + 1..].trim().to_string();
48 if !namespace.is_empty() && !ref_id.is_empty() {
49 return Some((namespace, ref_id));
50 }
51 }
52 Some((String::new(), trimmed.to_string()))
54}
55
56pub fn expand_alias(bare: &str, aliases: &ReferenceAliases) -> Option<String> {
57 aliases
58 .aliases
59 .get(bare)
60 .or_else(|| aliases.common.get(bare))
61 .cloned()
62}
63
64pub fn resolve_ref(reference: &str, ctx: &ResolverContext) -> ResolvedRef {
65 let original = reference.to_string();
66 let required = ctx.required_inputs.contains(&original);
67
68 let parsed = parse_ref(reference);
69 let parsed = match parsed {
70 Some(p) => p,
71 None => {
72 return ResolvedRef {
73 original,
74 namespace: String::new(),
75 ref_id: String::new(),
76 resolved_path: None,
77 confidence: 0.0,
78 required,
79 status: ResolutionStatus::Unresolved,
80 };
81 }
82 };
83
84 let (namespace, ref_id) = parsed;
85
86 let (namespace, ref_id) = if namespace.is_empty() {
88 match expand_alias(&ref_id, &ctx.aliases) {
89 Some(expanded) => match parse_ref(&expanded) {
90 Some((ns, id)) => (ns, id),
91 None => {
92 return ResolvedRef {
93 original,
94 namespace: String::new(),
95 ref_id: ref_id.clone(),
96 resolved_path: None,
97 confidence: 0.3,
98 required,
99 status: ResolutionStatus::Unresolved,
100 };
101 }
102 },
103 None => {
104 return ResolvedRef {
105 original,
106 namespace: String::new(),
107 ref_id: ref_id.clone(),
108 resolved_path: None,
109 confidence: 0.0,
110 required,
111 status: ResolutionStatus::Unresolved,
112 };
113 }
114 }
115 } else {
116 (namespace, ref_id)
117 };
118
119 match namespace.as_str() {
120 "file" => resolve_path_ref(&original, &ref_id, &ctx.host_root, required, "file", |p| {
121 p.exists()
122 }),
123 "folder" => resolve_path_ref(
124 &original,
125 &ref_id,
126 &ctx.host_root,
127 required,
128 "folder",
129 |p| p.is_dir(),
130 ),
131 _ => ResolvedRef {
132 original,
133 namespace,
134 ref_id,
135 resolved_path: None,
136 confidence: 0.0,
137 required,
138 status: ResolutionStatus::Unresolved,
139 },
140 }
141}
142
143pub fn resolve_all(packet: &PgnPacket, ctx: &ResolverContext) -> Vec<ResolvedRef> {
144 let mut results = Vec::new();
145
146 for field_name in &["in", "out"] {
147 if let Some(FieldValue::List(refs)) = packet.fields.get(*field_name) {
148 for reference in refs {
149 results.push(resolve_ref(reference, ctx));
150 }
151 }
152 }
153
154 results
155}
156
157fn resolve_symlink_target(path: &Path, depth: usize) -> Option<PathBuf> {
158 if depth == 0 || !path.is_symlink() {
159 return None;
160 }
161 let target = std::fs::read_link(path).ok()?;
162 let absolute = if target.is_absolute() {
163 target
164 } else if let Some(parent) = path.parent() {
165 parent.join(&target)
166 } else {
167 target
168 };
169 if absolute.is_symlink() {
170 resolve_symlink_target(&absolute, depth - 1)
171 } else {
172 Some(absolute)
173 }
174}
175
176fn is_path_within_root(candidate: &Path, root: &Path) -> bool {
177 let canonical_root = match root.canonicalize() {
178 Ok(r) => r,
179 Err(_) => return false,
180 };
181
182 if let Some(sym_target) = resolve_symlink_target(candidate, 8)
184 && !sym_target.starts_with(&canonical_root)
185 {
186 return false;
187 }
188
189 if let Ok(canonical) = candidate.canonicalize() {
191 return canonical.starts_with(&canonical_root);
192 }
193
194 let root_comps: Vec<_> = canonical_root.components().collect();
197 let candidate_comps: Vec<_> = candidate.components().collect();
198
199 if candidate_comps.len() < root_comps.len() {
200 return false;
201 }
202
203 for (i, rc) in root_comps.iter().enumerate() {
205 if candidate_comps.get(i) != Some(rc) {
206 return false;
207 }
208 }
209
210 let mut relative_depth: isize = 0;
212 for comp in &candidate_comps[root_comps.len()..] {
213 match comp {
214 std::path::Component::ParentDir => {
215 relative_depth -= 1;
216 if relative_depth < 0 {
217 return false;
218 }
219 }
220 std::path::Component::Normal(_) | std::path::Component::CurDir => {
221 relative_depth += 1;
222 }
223 _ => return false,
224 }
225 }
226
227 true
228}
229
230fn resolve_path_ref(
231 original: &str,
232 ref_id: &str,
233 host_root: &Path,
234 required: bool,
235 namespace: &str,
236 exists_fn: fn(&Path) -> bool,
237) -> ResolvedRef {
238 let canonical_root = match host_root.canonicalize() {
239 Ok(r) => r,
240 Err(_) => {
241 return ResolvedRef {
242 original: original.to_string(),
243 namespace: namespace.to_string(),
244 ref_id: ref_id.to_string(),
245 resolved_path: None,
246 confidence: 0.0,
247 required,
248 status: ResolutionStatus::Forbidden,
249 };
250 }
251 };
252 let resolved_path = canonical_root.join(ref_id);
253
254 if !is_path_within_root(&resolved_path, &canonical_root) {
255 return ResolvedRef {
256 original: original.to_string(),
257 namespace: namespace.to_string(),
258 ref_id: ref_id.to_string(),
259 resolved_path: None,
260 confidence: 0.0,
261 required,
262 status: ResolutionStatus::Forbidden,
263 };
264 }
265
266 let (status, confidence) = if exists_fn(&resolved_path) {
267 (ResolutionStatus::Resolved, 1.0)
268 } else {
269 (ResolutionStatus::Missing, 0.0)
270 };
271
272 ResolvedRef {
273 original: original.to_string(),
274 namespace: namespace.to_string(),
275 ref_id: ref_id.to_string(),
276 resolved_path: Some(resolved_path),
277 confidence,
278 required,
279 status,
280 }
281}
282
283pub fn load_aliases(path: &Path) -> Result<ReferenceAliases, crate::registry::ConfigError> {
284 let metadata = std::fs::metadata(path)?;
285 if metadata.len() > 10_485_760 {
286 return Err(crate::registry::ConfigError::Io(std::io::Error::new(
287 std::io::ErrorKind::InvalidData,
288 format!(
289 "config file {} exceeds maximum size ({} > 10 MiB)",
290 path.display(),
291 metadata.len()
292 ),
293 )));
294 }
295 let content = std::fs::read_to_string(path)?;
296 let aliases: ReferenceAliases = serde_yaml::from_str(&content)?;
297 Ok(aliases)
298}