Mountain/ExtensionManagement/
Scanner.rs1use std::{path::PathBuf, sync::Arc};
116
117use CommonLibrary::{
118 Effect::ApplicationRunTime::ApplicationRunTime as _,
119 Error::CommonError::CommonError,
120 FileSystem::{DTO::FileTypeDTO::FileTypeDTO, ReadDirectory::ReadDirectory, ReadFile::ReadFile},
121};
122use serde_json::{Map, Value};
123use tauri::Manager;
124
125use crate::{
126 ApplicationState::{ApplicationState, DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO},
127 Environment::Utility,
128 RunTime::ApplicationRunTime::ApplicationRunTime,
129 dev_log,
130};
131
132pub async fn ScanDirectoryForExtensions(
138 ApplicationHandle:tauri::AppHandle,
139
140 DirectoryPath:PathBuf,
141) -> Result<Vec<ExtensionDescriptionStateDTO>, CommonError> {
142 let RunTime = ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
143
144 let mut FoundExtensions = Vec::new();
145
146 match DirectoryPath.try_exists() {
150 Ok(false) => {
151 dev_log!(
152 "extensions",
153 "[ExtensionScanner] Extension path '{}' does not exist, skipping (no extensions installed here)",
154 DirectoryPath.display()
155 );
156 return Ok(Vec::new());
157 },
158 Err(error) => {
159 dev_log!(
160 "extensions",
161 "[ExtensionScanner] Could not stat extension path '{}': {} — skipping",
162 DirectoryPath.display(),
163 error
164 );
165 return Ok(Vec::new());
166 },
167 Ok(true) => {},
168 }
169
170 let TopLevelEntries = match RunTime.Run(ReadDirectory(DirectoryPath.clone())).await {
171 Ok(entries) => entries,
172
173 Err(error) => {
174 dev_log!(
175 "extensions",
176 "warn: [ExtensionScanner] Could not read extension directory '{}': {}. Skipping.",
177 DirectoryPath.display(),
178 error
179 );
180
181 return Ok(Vec::new());
182 },
183 };
184
185 dev_log!(
186 "extensions",
187 "[ExtensionScanner] Directory '{}' contains {} top-level entries",
188 DirectoryPath.display(),
189 TopLevelEntries.len()
190 );
191
192 let mut parse_failures = 0usize;
193 let mut missing_package_json = 0usize;
194
195 for (EntryName, FileType) in TopLevelEntries {
196 if FileType == FileTypeDTO::Directory {
197 let PotentialExtensionPath = DirectoryPath.join(EntryName);
198
199 let PackageJsonPath = PotentialExtensionPath.join("package.json");
200
201 dev_log!(
202 "extensions",
203 "[ExtensionScanner] Checking for package.json in: {}",
204 PotentialExtensionPath.display()
205 );
206
207 match RunTime.Run(ReadFile(PackageJsonPath.clone())).await {
208 Ok(PackageJsonContent) => {
209 let mut ManifestValue:Value = match serde_json::from_slice::<Value>(&PackageJsonContent) {
215 Ok(v) => v,
216 Err(error) => {
217 parse_failures += 1;
218 dev_log!(
219 "extensions",
220 "warn: [ExtensionScanner] Failed to parse package.json at '{}': {}",
221 PotentialExtensionPath.display(),
222 error
223 );
224 continue;
225 },
226 };
227
228 if let Some(NLSMap) = LoadNLSBundle(&RunTime, &PotentialExtensionPath).await {
229 let mut Replaced = 0u32;
230 let mut Unresolved = 0u32;
231 ResolveNLSPlaceholdersInner(&mut ManifestValue, &NLSMap, &mut Replaced, &mut Unresolved);
232 dev_log!(
233 "extensions",
234 "[LandFix:NLS] {} → {} replaced, {} unresolved placeholders",
235 PotentialExtensionPath.display(),
236 Replaced,
237 Unresolved
238 );
239 }
240
241 match serde_json::from_value::<ExtensionDescriptionStateDTO>(ManifestValue) {
242 Ok(mut Description) => {
243 Description.ExtensionLocation =
245 serde_json::to_value(url::Url::from_directory_path(&PotentialExtensionPath).unwrap())
246 .unwrap_or(Value::Null);
247
248 if Description.Identifier == Value::Null
250 || Description.Identifier == Value::Object(Default::default())
251 {
252 let Id = if Description.Publisher.is_empty() {
253 Description.Name.clone()
254 } else {
255 format!("{}.{}", Description.Publisher, Description.Name)
256 };
257 Description.Identifier = serde_json::json!({ "value": Id });
258 }
259
260 Description.IsBuiltin = true;
262
263 FoundExtensions.push(Description);
264 },
265
266 Err(error) => {
267 parse_failures += 1;
268 dev_log!(
269 "extensions",
270 "warn: [ExtensionScanner] Failed to parse package.json for extension at '{}': {}",
271 PotentialExtensionPath.display(),
272 error
273 );
274 },
275 }
276 },
277 Err(error) => {
278 missing_package_json += 1;
279 dev_log!(
280 "extensions",
281 "warn: [ExtensionScanner] Could not read package.json at '{}': {}",
282 PackageJsonPath.display(),
283 error
284 );
285 },
286 }
287 }
288 }
289
290 dev_log!(
291 "extensions",
292 "[ExtensionScanner] Directory '{}' scan done: {} parsed, {} parse-failures, {} missing package.json",
293 DirectoryPath.display(),
294 FoundExtensions.len(),
295 parse_failures,
296 missing_package_json
297 );
298
299 Ok(FoundExtensions)
300}
301
302async fn LoadNLSBundle(RunTime:&Arc<ApplicationRunTime>, ExtensionPath:&PathBuf) -> Option<Map<String, Value>> {
307 let NLSPath = ExtensionPath.join("package.nls.json");
308 let Content = match RunTime.Run(ReadFile(NLSPath.clone())).await {
309 Ok(Bytes) => Bytes,
310 Err(Error) => {
311 dev_log!(
312 "extensions",
313 "[LandFix:NLS] no bundle for {} ({})",
314 ExtensionPath.display(),
315 Error
316 );
317 return None;
318 },
319 };
320 let Parsed:Value = match serde_json::from_slice(&Content) {
321 Ok(V) => V,
322 Err(Error) => {
323 dev_log!(
324 "extensions",
325 "warn: [LandFix:NLS] failed to parse {}: {}",
326 NLSPath.display(),
327 Error
328 );
329 return None;
330 },
331 };
332 let Object = Parsed.as_object()?;
333 let mut Resolved = Map::with_capacity(Object.len());
334 for (Key, RawValue) in Object {
335 let Text = if let Some(s) = RawValue.as_str() {
336 Some(s.to_string())
337 } else if let Some(obj) = RawValue.as_object() {
338 obj.get("message").and_then(|m| m.as_str()).map(|s| s.to_string())
339 } else {
340 None
341 };
342 if let Some(t) = Text {
343 Resolved.insert(Key.clone(), Value::String(t));
344 }
345 }
346 dev_log!(
347 "extensions",
348 "[LandFix:NLS] loaded {} keys for {}",
349 Resolved.len(),
350 ExtensionPath.display()
351 );
352 Some(Resolved)
353}
354
355fn ResolveNLSPlaceholders(Value:&mut Value, NLS:&Map<String, Value>) {
361 ResolveNLSPlaceholdersInner(Value, NLS, &mut 0u32, &mut 0u32);
362}
363
364fn ResolveNLSPlaceholdersInner(Value:&mut Value, NLS:&Map<String, Value>, Replaced:&mut u32, Unresolved:&mut u32) {
368 match Value {
369 serde_json::Value::String(Text) => {
370 if Text.len() >= 2 && Text.starts_with('%') && Text.ends_with('%') {
371 let Key = &Text[1..Text.len() - 1];
372 if !Key.is_empty() && !Key.contains('%') {
373 if let Some(Replacement) = NLS.get(Key).and_then(|v| v.as_str()) {
374 *Text = Replacement.to_string();
375 *Replaced += 1;
376 } else {
377 *Unresolved += 1;
378 }
379 }
380 }
381 },
382 serde_json::Value::Array(Items) => {
383 for Item in Items {
384 ResolveNLSPlaceholdersInner(Item, NLS, Replaced, Unresolved);
385 }
386 },
387 serde_json::Value::Object(Map) => {
388 for (_, FieldValue) in Map {
389 ResolveNLSPlaceholdersInner(FieldValue, NLS, Replaced, Unresolved);
390 }
391 },
392 _ => {},
393 }
394}
395
396pub fn CollectDefaultConfigurations(State:&ApplicationState) -> Result<Value, CommonError> {
399 let mut MergedDefaults = Map::new();
400
401 let Extensions = State
402 .Extension
403 .ScannedExtensions
404 .ScannedExtensions
405 .lock()
406 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
407
408 for Extension in Extensions.values() {
409 if let Some(contributes) = Extension.Contributes.as_ref().and_then(|v| v.as_object()) {
410 if let Some(configuration) = contributes.get("configuration").and_then(|v| v.as_object()) {
411 if let Some(properties) = configuration.get("properties").and_then(|v| v.as_object()) {
412 self::process_configuration_properties(&mut MergedDefaults, "", properties, &mut Vec::new())?;
414 }
415 }
416 }
417 }
418
419 Ok(Value::Object(MergedDefaults))
420}
421
422fn process_configuration_properties(
424 merged_defaults:&mut serde_json::Map<String, Value>,
425 current_path:&str,
426 properties:&serde_json::Map<String, Value>,
427 visited_keys:&mut Vec<String>,
428) -> Result<(), CommonError> {
429 for (key, value) in properties {
430 let full_path = if current_path.is_empty() {
432 key.clone()
433 } else {
434 format!("{}.{}", current_path, key)
435 };
436
437 if visited_keys.contains(&full_path) {
439 return Err(CommonError::Unknown {
440 Description:format!("Circular reference detected in configuration properties: {}", full_path),
441 });
442 }
443
444 visited_keys.push(full_path.clone());
445
446 if let Some(prop_details) = value.as_object() {
447 if let Some(nested_properties) = prop_details.get("properties").and_then(|v| v.as_object()) {
449 self::process_configuration_properties(merged_defaults, &full_path, nested_properties, visited_keys)?;
451 } else if let Some(default_value) = prop_details.get("default") {
452 merged_defaults.insert(full_path.clone(), default_value.clone());
454 }
455 }
456
457 visited_keys.retain(|k| k != &full_path);
459 }
460
461 Ok(())
462}