Files
launcher-next/Launcher.Next.Core/LauncherPaths.cs
T

166 lines
9.5 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));
}
public interface IPathResolver
{
ResolvedLauncherPaths Resolve(LauncherPathVariables variables, IEnumerable<string>? required = null);
string ResolvePath(string value, LauncherPathVariables variables, string? constrainedRoot = null);
}
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);
foreach (var name in variables.Values.Keys) resolved[name] = 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;
}
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 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();
}
}
}