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::{
127 DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO,
128 State::ApplicationState::ApplicationState,
129 },
130 Environment::Utility,
131 RunTime::ApplicationRunTime::ApplicationRunTime,
132 dev_log,
133};
134
135const EXTENSION_SCAN_DENY_LIST:&[&str] = &["types", "out", "node_modules", "test", ".vscode-test", ".git"];
144
145const TEST_ONLY_EXTENSIONS:&[&str] = &[
150 "vscode-api-tests",
151 "vscode-test-resolver",
152 "vscode-colorize-tests",
153 "vscode-colorize-perf-tests",
154 "vscode-notebook-tests",
155];
156
157fn IncludeTestExtensions() -> bool { matches!(std::env::var("Test").as_deref(), Ok("1") | Ok("true")) }
158
159fn IsDeniedDirectory(Name:&str) -> bool { EXTENSION_SCAN_DENY_LIST.iter().any(|Denied| *Denied == Name) }
160
161fn IsTestOnlyExtension(Name:&str) -> bool { TEST_ONLY_EXTENSIONS.iter().any(|TestOnly| *TestOnly == Name) }
162
163fn IsUserExtensionScanPath(DirectoryPath:&std::path::Path) -> bool {
181 let Normalised = match DirectoryPath.canonicalize() {
182 Ok(Canonical) => Canonical,
183
184 Err(_) => DirectoryPath.to_path_buf(),
185 };
186
187 if let Ok(Override) = std::env::var("Lodge") {
189 if !Override.is_empty() && Normalised == std::path::PathBuf::from(&Override) {
190 return true;
191 }
192 }
193
194 if let Ok(Home) = std::env::var("HOME") {
198 let UserRoot = std::path::PathBuf::from(Home).join(".land/extensions");
199
200 if Normalised == UserRoot {
201 return true;
202 }
203 }
204
205 false
206}
207
208pub async fn ScanDirectoryForExtensions(
214 ApplicationHandle:tauri::AppHandle,
215
216 DirectoryPath:PathBuf,
217) -> Result<Vec<ExtensionDescriptionStateDTO>, CommonError> {
218 let IsUserPath = IsUserExtensionScanPath(&DirectoryPath);
222
223 let RunTime = ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
224
225 let mut FoundExtensions = Vec::new();
226
227 match DirectoryPath.try_exists() {
231 Ok(false) => {
232 dev_log!(
233 "extensions",
234 "[ExtensionScanner] Extension path '{}' does not exist, skipping (no extensions installed here)",
235 DirectoryPath.display()
236 );
237
238 return Ok(Vec::new());
239 },
240
241 Err(error) => {
242 dev_log!(
243 "extensions",
244 "[ExtensionScanner] Could not stat extension path '{}': {} - skipping",
245 DirectoryPath.display(),
246 error
247 );
248
249 return Ok(Vec::new());
250 },
251
252 Ok(true) => {},
253 }
254
255 let TopLevelEntries = match RunTime.Run(ReadDirectory(DirectoryPath.clone())).await {
256 Ok(entries) => entries,
257
258 Err(error) => {
259 dev_log!(
260 "extensions",
261 "warn: [ExtensionScanner] Could not read extension directory '{}': {}. Skipping.",
262 DirectoryPath.display(),
263 error
264 );
265
266 return Ok(Vec::new());
267 },
268 };
269
270 dev_log!(
271 "extensions",
272 "[ExtensionScanner] Directory '{}' contains {} top-level entries",
273 DirectoryPath.display(),
274 TopLevelEntries.len()
275 );
276
277 let mut parse_failures = 0usize;
278
279 let mut missing_package_json = 0usize;
280
281 let mut denied_directory_count = 0usize;
282
283 let mut test_extension_skips = 0usize;
284
285 let AllowTestExtensions = IncludeTestExtensions();
286
287 for (EntryName, FileType) in TopLevelEntries {
288 if FileType == FileTypeDTO::Directory {
289 if IsDeniedDirectory(&EntryName) {
292 denied_directory_count += 1;
293
294 continue;
295 }
296
297 if !AllowTestExtensions && IsTestOnlyExtension(&EntryName) {
298 test_extension_skips += 1;
299
300 continue;
301 }
302
303 let PotentialExtensionPath = DirectoryPath.join(EntryName);
304
305 let PackageJsonPath = PotentialExtensionPath.join("package.json");
306
307 dev_log!(
312 "ext-scan-verbose",
313 "[ExtensionScanner] Checking for package.json in: {}",
314 PotentialExtensionPath.display()
315 );
316
317 match RunTime.Run(ReadFile(PackageJsonPath.clone())).await {
318 Ok(PackageJsonContent) => {
319 let mut ManifestValue:Value = match serde_json::from_slice::<Value>(&PackageJsonContent) {
325 Ok(v) => v,
326
327 Err(error) => {
328 parse_failures += 1;
329
330 dev_log!(
331 "extensions",
332 "warn: [ExtensionScanner] Failed to parse package.json at '{}': {}",
333 PotentialExtensionPath.display(),
334 error
335 );
336
337 continue;
338 },
339 };
340
341 let ManifestUsesPlaceholders = ManifestContainsNLSPlaceholders(&ManifestValue);
348
349 if let Some(NLSMap) =
350 LoadNLSBundle(&RunTime, &PotentialExtensionPath, ManifestUsesPlaceholders).await
351 {
352 let mut Replaced = 0u32;
353
354 let mut Unresolved = 0u32;
355
356 ResolveNLSPlaceholdersInner(&mut ManifestValue, &NLSMap, &mut Replaced, &mut Unresolved);
357
358 dev_log!(
359 "nls",
360 "[LandFix:NLS] {} → {} replaced, {} unresolved placeholders",
361 PotentialExtensionPath.display(),
362 Replaced,
363 Unresolved
364 );
365 }
366
367 match serde_json::from_value::<ExtensionDescriptionStateDTO>(ManifestValue) {
368 Ok(mut Description) => {
369 Description.ExtensionLocation =
371 serde_json::to_value(url::Url::from_directory_path(&PotentialExtensionPath).unwrap())
372 .unwrap_or(Value::Null);
373
374 if Description.Identifier == Value::Null
376 || Description.Identifier == Value::Object(Default::default())
377 {
378 let Id = if Description.Publisher.is_empty() {
379 Description.Name.clone()
380 } else {
381 format!("{}.{}", Description.Publisher, Description.Name)
382 };
383
384 Description.Identifier = serde_json::json!({ "value": Id });
385 }
386
387 Description.IsBuiltin = !IsUserPath;
398
399 #[cfg(unix)]
409 if IsUserPath {
410 crate::ExtensionManagement::VsixInstaller::HealExecutableBits(&PotentialExtensionPath);
411 }
412
413 dev_log!(
414 "ext-scan",
415 "[ExtScan] accept path={} is_user={} is_builtin={} id={}",
416 PotentialExtensionPath.display(),
417 IsUserPath,
418 Description.IsBuiltin,
419 Description
420 .Identifier
421 .get("value")
422 .and_then(|V| V.as_str())
423 .unwrap_or("<unknown>")
424 );
425
426 FoundExtensions.push(Description);
427 },
428
429 Err(error) => {
430 parse_failures += 1;
431
432 dev_log!(
433 "extensions",
434 "warn: [ExtensionScanner] Failed to parse package.json for extension at '{}': {}",
435 PotentialExtensionPath.display(),
436 error
437 );
438
439 dev_log!(
440 "ext-scan",
441 "[ExtScan] skip path={} reason=parse-failure err={}",
442 PotentialExtensionPath.display(),
443 error
444 );
445 },
446 }
447 },
448
449 Err(error) => {
450 missing_package_json += 1;
451
452 dev_log!(
453 "extensions",
454 "warn: [ExtensionScanner] Could not read package.json at '{}': {}",
455 PackageJsonPath.display(),
456 error
457 );
458
459 dev_log!(
460 "ext-scan",
461 "[ExtScan] skip path={} reason=no-package-json err={}",
462 PotentialExtensionPath.display(),
463 error
464 );
465 },
466 }
467 }
468 }
469
470 dev_log!(
471 "extensions",
472 "[ExtensionScanner] Directory '{}' scan done: {} parsed, {} parse-failures, {} missing package.json, {} \
473 denied-dirs, {} test-extensions-skipped (Test={})",
474 DirectoryPath.display(),
475 FoundExtensions.len(),
476 parse_failures,
477 missing_package_json,
478 denied_directory_count,
479 test_extension_skips,
480 AllowTestExtensions,
481 );
482
483 Ok(FoundExtensions)
484}
485
486fn ManifestContainsNLSPlaceholders(Value:&Value) -> bool {
490 match Value {
491 serde_json::Value::String(Text) => {
492 Text.len() >= 2 && Text.starts_with('%') && Text.ends_with('%') && !Text[1..Text.len() - 1].contains('%')
493 },
494
495 serde_json::Value::Array(Items) => Items.iter().any(ManifestContainsNLSPlaceholders),
496
497 serde_json::Value::Object(Object) => Object.values().any(ManifestContainsNLSPlaceholders),
498
499 _ => false,
500 }
501}
502
503async fn LoadNLSBundle(
513 RunTime:&Arc<ApplicationRunTime>,
514
515 ExtensionPath:&PathBuf,
516
517 PlaceholdersNeeded:bool,
518) -> Option<Map<String, Value>> {
519 let NLSPath = ExtensionPath.join("package.nls.json");
520
521 let Content = match RunTime.Run(ReadFile(NLSPath.clone())).await {
522 Ok(Bytes) => Bytes,
523
524 Err(Error) => {
525 if PlaceholdersNeeded {
526 dev_log!("nls", "[LandFix:NLS] no bundle for {} ({})", ExtensionPath.display(), Error);
527 } else {
528 dev_log!(
529 "nls",
530 "[LandFix:NLS] {} has no placeholders, no bundle needed",
531 ExtensionPath.display()
532 );
533 }
534
535 return None;
536 },
537 };
538
539 let Parsed:Value = match serde_json::from_slice(&Content) {
540 Ok(V) => V,
541
542 Err(Error) => {
543 dev_log!("nls", "warn: [LandFix:NLS] failed to parse {}: {}", NLSPath.display(), Error);
544
545 return None;
546 },
547 };
548
549 let Object = Parsed.as_object()?;
550
551 let mut Resolved = Map::with_capacity(Object.len());
552
553 for (Key, RawValue) in Object {
554 let Text = if let Some(s) = RawValue.as_str() {
555 Some(s.to_string())
556 } else if let Some(obj) = RawValue.as_object() {
557 obj.get("message").and_then(|m| m.as_str()).map(|s| s.to_string())
558 } else {
559 None
560 };
561
562 if let Some(t) = Text {
563 Resolved.insert(Key.clone(), Value::String(t));
564 }
565 }
566
567 dev_log!(
568 "nls",
569 "[LandFix:NLS] loaded {} keys for {}",
570 Resolved.len(),
571 ExtensionPath.display()
572 );
573
574 Some(Resolved)
575}
576
577fn ResolveNLSPlaceholdersInner(Value:&mut Value, NLS:&Map<String, Value>, Replaced:&mut u32, Unresolved:&mut u32) {
581 match Value {
582 serde_json::Value::String(Text) => {
583 if Text.len() >= 2 && Text.starts_with('%') && Text.ends_with('%') {
584 let Key = &Text[1..Text.len() - 1];
585
586 if !Key.is_empty() && !Key.contains('%') {
587 if let Some(Replacement) = NLS.get(Key).and_then(|v| v.as_str()) {
588 *Text = Replacement.to_string();
589 *Replaced += 1;
590 } else {
591 *Unresolved += 1;
592 }
593 }
594 }
595 },
596
597 serde_json::Value::Array(Items) => {
598 for Item in Items {
599 ResolveNLSPlaceholdersInner(Item, NLS, Replaced, Unresolved);
600 }
601 },
602
603 serde_json::Value::Object(Map) => {
604 for (_, FieldValue) in Map {
605 ResolveNLSPlaceholdersInner(FieldValue, NLS, Replaced, Unresolved);
606 }
607 },
608
609 _ => {},
610 }
611}
612
613pub fn CollectDefaultConfigurations(State:&ApplicationState) -> Result<Value, CommonError> {
616 let mut MergedDefaults = Map::new();
617
618 let Extensions = State
619 .Extension
620 .ScannedExtensions
621 .ScannedExtensions
622 .lock()
623 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
624
625 for Extension in Extensions.values() {
626 if let Some(contributes) = Extension.Contributes.as_ref().and_then(|v| v.as_object()) {
627 if let Some(configuration) = contributes.get("configuration").and_then(|v| v.as_object()) {
628 if let Some(properties) = configuration.get("properties").and_then(|v| v.as_object()) {
629 self::process_configuration_properties(&mut MergedDefaults, "", properties, &mut Vec::new())?;
631 }
632 }
633 }
634 }
635
636 Ok(Value::Object(MergedDefaults))
637}
638
639fn process_configuration_properties(
641 merged_defaults:&mut serde_json::Map<String, Value>,
642
643 current_path:&str,
644
645 properties:&serde_json::Map<String, Value>,
646
647 visited_keys:&mut Vec<String>,
648) -> Result<(), CommonError> {
649 for (key, value) in properties {
650 let full_path = if current_path.is_empty() {
652 key.clone()
653 } else {
654 format!("{}.{}", current_path, key)
655 };
656
657 if visited_keys.contains(&full_path) {
659 return Err(CommonError::Unknown {
660 Description:format!("Circular reference detected in configuration properties: {}", full_path),
661 });
662 }
663
664 visited_keys.push(full_path.clone());
665
666 if let Some(prop_details) = value.as_object() {
667 if let Some(nested_properties) = prop_details.get("properties").and_then(|v| v.as_object()) {
669 self::process_configuration_properties(merged_defaults, &full_path, nested_properties, visited_keys)?;
671 } else if let Some(default_value) = prop_details.get("default") {
672 merged_defaults.insert(full_path.clone(), default_value.clone());
674 }
675 }
676
677 visited_keys.retain(|k| k != &full_path);
679 }
680
681 Ok(())
682}