241 lines
16 KiB
C#
241 lines
16 KiB
C#
using System.Text.RegularExpressions;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace Launcher.Next.Core;
|
|
|
|
public sealed record LauncherPathVariables(IReadOnlyDictionary<string, string?> Values)
|
|
{
|
|
public string? this[string name] => Values.TryGetValue(name, out var value) ? value : null;
|
|
}
|
|
|
|
public sealed record ResolvedLauncherPaths(IReadOnlyDictionary<string, string?> Values)
|
|
{
|
|
public string Require(string name) => Values.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)
|
|
? value : throw new InvalidOperationException($"PATH_VARIABLE_REQUIRED:{name}");
|
|
public string? Get(string name) => Values.TryGetValue(name, out var value) ? value : null;
|
|
public string LocalLauncherRoot => Require(nameof(LocalLauncherRoot));
|
|
public string AcadExecutable => Require(nameof(AcadExecutable));
|
|
public string? AutomationScriptPath => Get(nameof(AutomationScriptPath));
|
|
public string? FixtureOrthoDwg => Get(nameof(FixtureOrthoDwg));
|
|
}
|
|
|
|
/// <summary>Catalog source role resolved by <see cref="IPathResolver"/>. Raw
|
|
/// filesystem strings are intentionally not accepted by catalog acquisition.</summary>
|
|
public sealed record ResolvedCatalogRoot
|
|
{
|
|
internal ResolvedCatalogRoot(string value) => Value = value;
|
|
public string Value { get; }
|
|
}
|
|
public sealed record ResolvedLegacySettingsPath { internal ResolvedLegacySettingsPath(string value) => Value=value; public string Value { get; } }
|
|
public sealed record ResolvedLegacyDownloadListPath { internal ResolvedLegacyDownloadListPath(string value) => Value=value; public string Value { get; } }
|
|
public sealed record ResolvedLegacyCompatibilityOverlayPath { internal ResolvedLegacyCompatibilityOverlayPath(string value) => Value=value; public string Value { get; } }
|
|
public sealed record ResolvedLegacyProbeEvidenceRoot { internal ResolvedLegacyProbeEvidenceRoot(string value) => Value=value; public string Value { get; } }
|
|
public sealed record ResolvedLocalPackageCacheRoot { internal ResolvedLocalPackageCacheRoot(string value)=>Value=value; public string Value{get;} }
|
|
public sealed record ResolvedPackageVersionStoreRoot { internal ResolvedPackageVersionStoreRoot(string value)=>Value=value; public string Value{get;} }
|
|
public sealed record ResolvedPackageStagingRoot { internal ResolvedPackageStagingRoot(string value)=>Value=value; public string Value{get;} }
|
|
public sealed record ResolvedActivePackageRoot { internal ResolvedActivePackageRoot(string value)=>Value=value; public string Value{get;} }
|
|
public sealed record ResolvedPackageLifecycleEvidenceRoot { internal ResolvedPackageLifecycleEvidenceRoot(string value)=>Value=value; public string Value{get;} }
|
|
public sealed record ResolvedPackagePilotRoot { internal ResolvedPackagePilotRoot(string value)=>Value=value; public string Value{get;} }
|
|
public sealed record ResolvedPackageCatalogRoot { internal ResolvedPackageCatalogRoot(string value)=>Value=value; public string Value{get;} }
|
|
public sealed record ResolvedImmutablePackagePath { internal ResolvedImmutablePackagePath(string value)=>Value=value; public string Value{get;} }
|
|
public sealed record ResolvedPackageLifecycleRoots(
|
|
ResolvedPackagePilotRoot Pilot,
|
|
ResolvedPackageCatalogRoot Catalog,
|
|
ResolvedPackageStagingRoot Staging,
|
|
ResolvedPackageVersionStoreRoot VersionStore,
|
|
ResolvedActivePackageRoot Active,
|
|
ResolvedPackageLifecycleEvidenceRoot Evidence);
|
|
|
|
public interface IPathResolver
|
|
{
|
|
ResolvedLauncherPaths Resolve(LauncherPathVariables variables, IEnumerable<string>? required = null);
|
|
string ResolvePath(string value, LauncherPathVariables variables, string? constrainedRoot = null);
|
|
ResolvedCatalogRoot ResolveCatalogRoot(LauncherPathVariables variables);
|
|
ResolvedLegacySettingsPath ResolveLegacySettingsPath(LauncherPathVariables variables);
|
|
ResolvedLegacyDownloadListPath ResolveLegacyDownloadListPath(LauncherPathVariables variables);
|
|
ResolvedLegacyCompatibilityOverlayPath ResolveLegacyCompatibilityOverlayPath(LauncherPathVariables variables);
|
|
ResolvedLegacyProbeEvidenceRoot ResolveLegacyProbeEvidenceRoot(LauncherPathVariables variables);
|
|
ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value);
|
|
ResolvedPackageLifecycleRoots ResolvePackageLifecycleRoots(string value);
|
|
}
|
|
|
|
public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environment = null) : IPathResolver
|
|
{
|
|
private static readonly Regex Token = new(@"\$\{(?<name>[A-Za-z][A-Za-z0-9_]*)\}", RegexOptions.Compiled);
|
|
private readonly IReadOnlyDictionary<string, string?> environment = environment ?? Environment.GetEnvironmentVariables()
|
|
.Cast<System.Collections.DictionaryEntry>().ToDictionary(x => (string)x.Key, x => x.Value?.ToString(), StringComparer.OrdinalIgnoreCase);
|
|
|
|
public ResolvedLauncherPaths Resolve(LauncherPathVariables variables, IEnumerable<string>? required = null)
|
|
{
|
|
var resolved = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
|
// Explicit null values represent optional paths (for example the Apply
|
|
// permit during Preview). They are retained as null rather than being
|
|
// treated as an unknown variable; required values are still enforced
|
|
// below.
|
|
foreach (var name in variables.Values.Keys)
|
|
resolved[name] = variables[name] is null ? null : ResolveVariable(name, variables, resolved, []);
|
|
foreach (var name in resolved.Keys.ToArray())
|
|
if (resolved[name] is { } value && value.StartsWith("A:\\", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var corporate = ResolveVariable(LauncherPathVariableNames.CorporateDfsRoot, variables, resolved, []) ?? throw new InvalidOperationException("PATH_VARIABLE_EMPTY:CorporateDfsRoot");
|
|
resolved[name] = Path.Combine(corporate, value[3..]);
|
|
}
|
|
foreach (var name in required ?? [])
|
|
if (!resolved.TryGetValue(name, out var value) || string.IsNullOrWhiteSpace(value))
|
|
throw new InvalidOperationException($"PATH_VARIABLE_REQUIRED:{name}");
|
|
return new(resolved);
|
|
}
|
|
|
|
public string ResolvePath(string value, LauncherPathVariables variables, string? constrainedRoot = null)
|
|
{
|
|
var resolved = Expand(value, variables, new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase), []);
|
|
if (string.IsNullOrWhiteSpace(resolved) || !Path.IsPathFullyQualified(resolved))
|
|
throw new InvalidOperationException("PATH_ABSOLUTE_REQUIRED");
|
|
if (resolved.StartsWith("A:\\", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var corporate = Resolve(variables, [LauncherPathVariableNames.CorporateDfsRoot]).Require(LauncherPathVariableNames.CorporateDfsRoot);
|
|
resolved = Path.Combine(corporate, resolved[3..]);
|
|
}
|
|
var full = Path.GetFullPath(resolved);
|
|
if (full.StartsWith("A:\\", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_DRIVE_A_UNRESOLVED");
|
|
if (!string.IsNullOrWhiteSpace(constrainedRoot))
|
|
{
|
|
var root = Path.GetFullPath(ResolvePath(constrainedRoot, variables));
|
|
if (!full.Equals(root, StringComparison.OrdinalIgnoreCase) && !full.StartsWith(root.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
|
throw new InvalidOperationException("PATH_OUTSIDE_CONSTRAINED_ROOT");
|
|
}
|
|
return full;
|
|
}
|
|
|
|
public ResolvedCatalogRoot ResolveCatalogRoot(LauncherPathVariables variables)
|
|
{
|
|
var resolved = Resolve(variables, [LauncherPathVariableNames.CorporateCatalogRoot, LauncherPathVariableNames.CorporateDfsRoot]);
|
|
var catalog = ResolvePath(resolved.Require(LauncherPathVariableNames.CorporateCatalogRoot), variables);
|
|
var corporate = ResolvePath(resolved.Require(LauncherPathVariableNames.CorporateDfsRoot), variables);
|
|
if (!catalog.StartsWith(corporate.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
|
throw new InvalidOperationException("PATH_ROLE_COLLISION");
|
|
return new ResolvedCatalogRoot(catalog);
|
|
}
|
|
public ResolvedLegacySettingsPath ResolveLegacySettingsPath(LauncherPathVariables variables) => new(ResolveExistingFile("LegacySettingsPath", variables));
|
|
public ResolvedLegacyDownloadListPath ResolveLegacyDownloadListPath(LauncherPathVariables variables) => new(ResolveExistingFile("LegacyDownloadListPath", variables));
|
|
public ResolvedLegacyCompatibilityOverlayPath ResolveLegacyCompatibilityOverlayPath(LauncherPathVariables variables) => new(ResolveExistingFile("LegacyCompatibilityOverlayPath", variables));
|
|
public ResolvedLegacyProbeEvidenceRoot ResolveLegacyProbeEvidenceRoot(LauncherPathVariables variables)
|
|
{
|
|
var value=ResolvePath(Resolve(variables,["LegacyProbeEvidenceRoot"]).Require("LegacyProbeEvidenceRoot"),variables);
|
|
if(value.StartsWith(@"\\",StringComparison.Ordinal)) throw new InvalidOperationException("LEGACY_PROBE_EVIDENCE_ROOT_NOT_LOCAL");
|
|
return new(value);
|
|
}
|
|
public ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value)
|
|
{ if (string.IsNullOrWhiteSpace(value)||Uri.IsWellFormedUriString(value,UriKind.Absolute)||value.Contains('*')||value.Contains('?')||value.StartsWith(@"\\")||!Path.IsPathFullyQualified(value)) throw new InvalidOperationException("PACKAGE_PILOT_ROOT_NOT_LOCAL"); var full=Path.GetFullPath(value); var info=new DirectoryInfo(full); if(info.Exists&&info.Attributes.HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("PACKAGE_ROOT_REPARSE_REJECTED"); return new(full); }
|
|
public ResolvedPackageLifecycleRoots ResolvePackageLifecycleRoots(string value)
|
|
{
|
|
var pilot=ResolvePackagePilotRoot(value); string Child(string name)=>Path.GetFullPath(Path.Combine(pilot.Value,name));
|
|
var paths=new[]{Child("catalog"),Child("staging"),Child("version-store"),Child("active"),Child("evidence")};
|
|
if(paths.Distinct(StringComparer.OrdinalIgnoreCase).Count()!=paths.Length) throw new InvalidOperationException("PACKAGE_ROOT_COLLISION");
|
|
foreach(var path in paths) if(!path.StartsWith(pilot.Value.TrimEnd(Path.DirectorySeparatorChar)+Path.DirectorySeparatorChar,StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("PACKAGE_ROOT_TOPOLOGY_INVALID");
|
|
return new(pilot,new(paths[0]),new(paths[1]),new(paths[2]),new(paths[3]),new(paths[4]));
|
|
}
|
|
private string ResolveExistingFile(string role, LauncherPathVariables variables)
|
|
{
|
|
var value=ResolvePath(Resolve(variables,[role]).Require(role),variables);
|
|
if(!File.Exists(value) || Directory.Exists(value)) throw new InvalidOperationException("LEGACY_PROBE_INPUT_NOT_FILE");
|
|
return value;
|
|
}
|
|
|
|
private string? ResolveVariable(string name, LauncherPathVariables variables, Dictionary<string, string?> resolved, HashSet<string> stack)
|
|
{
|
|
if (resolved.TryGetValue(name, out var cached)) return cached;
|
|
if (!stack.Add(name)) throw new InvalidOperationException($"PATH_VARIABLE_CYCLE:{name}");
|
|
try
|
|
{
|
|
var source = variables[name] ?? (environment.TryGetValue(name, out var fromEnvironment) ? fromEnvironment : null);
|
|
if (source is null) throw new InvalidOperationException($"PATH_VARIABLE_UNKNOWN:{name}");
|
|
var expanded = Token.Replace(source, match => ResolveVariable(match.Groups["name"].Value, variables, resolved, stack) ?? throw new InvalidOperationException($"PATH_VARIABLE_EMPTY:{match.Groups["name"].Value}"));
|
|
expanded = expanded.Replace('/', Path.DirectorySeparatorChar);
|
|
resolved[name] = expanded;
|
|
return expanded;
|
|
}
|
|
finally { stack.Remove(name); }
|
|
}
|
|
|
|
private string Expand(string value, LauncherPathVariables variables, Dictionary<string, string?> resolved, HashSet<string> stack) =>
|
|
Token.Replace(value, match => ResolveVariable(match.Groups["name"].Value, variables, resolved, stack) ?? throw new InvalidOperationException($"PATH_VARIABLE_EMPTY:{match.Groups["name"].Value}"));
|
|
}
|
|
|
|
public static class LauncherPathVariableNames
|
|
{
|
|
public const string CorporateDfsRoot = "CorporateDfsRoot";
|
|
public const string CorporateLauncherRoot = "CorporateLauncherRoot";
|
|
public const string CorporateCatalogRoot = "CorporateCatalogRoot";
|
|
public const string CorporatePackagesRoot = "CorporatePackagesRoot";
|
|
public const string CorporateProfilesRoot = "CorporateProfilesRoot";
|
|
public const string CorporatePalettesRoot = "CorporatePalettesRoot";
|
|
public const string CorporateFontsRoot = "CorporateFontsRoot";
|
|
public const string CorporateTemplatesRoot = "CorporateTemplatesRoot";
|
|
public const string CorporateScriptsRoot = "CorporateScriptsRoot";
|
|
public const string ProjectName = "ProjectName";
|
|
public const string DisciplineName = "DisciplineName";
|
|
public const string ProjectRoot = "ProjectRoot";
|
|
public const string DisciplineRoot = "DisciplineRoot";
|
|
public const string PlantProjectFile = "PlantProjectFile";
|
|
public const string ProjectRosterPath = "ProjectRosterPath";
|
|
public const string ProjectApplicationsRoot = "ProjectApplicationsRoot";
|
|
public const string ProjectPaletteOverlayRoot = "ProjectPaletteOverlayRoot";
|
|
public const string LocalLauncherRoot = "LocalLauncherRoot";
|
|
public const string LocalPackageCache = "LocalPackageCache";
|
|
public const string LocalActiveRoot = "LocalActiveRoot";
|
|
public const string LocalSessionRoot = "LocalSessionRoot";
|
|
public const string LocalEvidenceRoot = "LocalEvidenceRoot";
|
|
public const string LocalLogsRoot = "LocalLogsRoot";
|
|
public const string LocalRollbackRoot = "LocalRollbackRoot";
|
|
public const string AcadExecutable = "AcadExecutable";
|
|
public const string PRecEntryAssembly = "PRecEntryAssembly";
|
|
public const string FixtureOrthoDwg = "FixtureOrthoDwg";
|
|
public const string FixtureModelDwg = "FixtureModelDwg";
|
|
public const string AutomationScriptPath = "AutomationScriptPath";
|
|
public const string AutomationConfigPath = "AutomationConfigPath";
|
|
public const string AutomationEvidencePath = "AutomationEvidencePath";
|
|
public const string AutomationPermitPath = "AutomationPermitPath";
|
|
public const string AutomationTranscriptPath = "AutomationTranscriptPath";
|
|
}
|
|
|
|
public static class LauncherConfigurationLayers
|
|
{
|
|
private static readonly HashSet<string> Protected = [LauncherPathVariableNames.CorporateDfsRoot, LauncherPathVariableNames.AcadExecutable, LauncherPathVariableNames.LocalLauncherRoot];
|
|
|
|
public static PilotConfigurationLoadResult Load(string? corporateDefaultsJson, string? machineJson, string? sessionJson)
|
|
{
|
|
try
|
|
{
|
|
var merged = Parse(corporateDefaultsJson);
|
|
Merge(merged, Parse(machineJson), false);
|
|
Merge(merged, Parse(sessionJson), false);
|
|
return PilotConfigurationParser.Parse(merged.ToJsonString());
|
|
}
|
|
catch (JsonException) { return new(PilotLocalConfiguration.SafeDefault, ["PILOT_CONFIGURATION_INVALID"]); }
|
|
}
|
|
|
|
public static PilotConfigurationLoadResult LoadFromFiles(string corporateDefaultsPath, string machinePath, string sessionPath)
|
|
{
|
|
var defaults = File.Exists(corporateDefaultsPath) ? File.ReadAllText(corporateDefaultsPath) : null;
|
|
var machine = File.Exists(machinePath) ? File.ReadAllText(machinePath) : null;
|
|
var session = File.Exists(sessionPath) ? File.ReadAllText(sessionPath) : null;
|
|
return Load(defaults, machine, session);
|
|
}
|
|
|
|
private static JsonObject Parse(string? json) => string.IsNullOrWhiteSpace(json) ? [] : JsonNode.Parse(json)?.AsObject() ?? [];
|
|
private static void Merge(JsonObject target, JsonObject overlay, bool roster)
|
|
{
|
|
foreach (var entry in overlay)
|
|
{
|
|
if (entry.Key.Equals("PathVariables", StringComparison.OrdinalIgnoreCase) && entry.Value is JsonObject paths)
|
|
{
|
|
var targetPaths = target["PathVariables"] as JsonObject ?? new JsonObject(); target["PathVariables"] = targetPaths;
|
|
foreach (var path in paths) if (!roster || !Protected.Contains(path.Key)) targetPaths[path.Key] = path.Value?.DeepClone();
|
|
}
|
|
else target[entry.Key] = entry.Value?.DeepClone();
|
|
}
|
|
}
|
|
}
|