From 97fbc1d746ac55d9e488d15bc75a4aa579d4b80d Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 6 Aug 2026 13:56:54 +0300 Subject: [PATCH] feat: acquire legacy settings from approved network source --- Launcher.Next.App/LocalParityLaunchService.cs | 11 +++- .../LegacyConfigurationAdapters.cs | 15 +++-- .../LegacyNetworkConfiguration.cs | 61 +++++++++++++++++++ scripts/Build-OwnerReviewDistribution.ps1 | 11 ++-- .../LegacyNetworkConfigurationTests.cs | 39 ++++++++++++ .../LegacyNetworkSettingsParserTests.cs | 29 +++++++++ 6 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 Launcher.Next.Application/LegacyNetworkConfiguration.cs create mode 100644 tests/Launcher.Next.Tests/LegacyNetworkConfigurationTests.cs create mode 100644 tests/Launcher.Next.Tests/LegacyNetworkSettingsParserTests.cs diff --git a/Launcher.Next.App/LocalParityLaunchService.cs b/Launcher.Next.App/LocalParityLaunchService.cs index 2f99af2..c8f57cc 100644 --- a/Launcher.Next.App/LocalParityLaunchService.cs +++ b/Launcher.Next.App/LocalParityLaunchService.cs @@ -21,7 +21,8 @@ namespace Launcher.Next.App; /// internal sealed record LocalParityLaunchProfile( int SchemaVersion, string Name, string Root, string SettingsPath, string DownloadListPath, - string OverlayPath, string CatalogRoot, string PatternName, string EvidenceRoot); + string OverlayPath, string CatalogRoot, string PatternName, string EvidenceRoot, string? NetworkSettingsEndpoint = null, + LegacySettingsSnapshot? SettingsSnapshot = null); internal sealed record LocalParityPrepared(LocalParityLaunchProfile Profile, PackageRequirement Requirement, PackageManifest Manifest, ImmutableLocalPackage Package, PackageActivationResult Activation, @@ -41,7 +42,13 @@ internal sealed class LocalParityLaunchService if (profile.SchemaVersion != 1 || string.IsNullOrWhiteSpace(profile.Name) || string.IsNullOrWhiteSpace(profile.PatternName)) throw new InvalidOperationException("LOCAL_PARITY_PROFILE_INVALID"); ValidateProfile(profile); - return profile with { Root = Path.GetFullPath(profile.Root) }; + profile = profile with { Root = Path.GetFullPath(profile.Root) }; + if (!string.IsNullOrWhiteSpace(profile.NetworkSettingsEndpoint)) + { + var snapshot = LegacyNetworkConfiguration.AcquireApprovedSnapshot(profile.NetworkSettingsEndpoint, profile.Root); + profile = profile with { SettingsPath = snapshot.LocalSnapshotPath, SettingsSnapshot = snapshot }; + } + return profile; } public async Task PrepareAsync(LocalParityLaunchProfile profile, CancellationToken ct) diff --git a/Launcher.Next.Application/LegacyConfigurationAdapters.cs b/Launcher.Next.Application/LegacyConfigurationAdapters.cs index 4119c8d..3625870 100644 --- a/Launcher.Next.Application/LegacyConfigurationAdapters.cs +++ b/Launcher.Next.Application/LegacyConfigurationAdapters.cs @@ -4,8 +4,11 @@ using Launcher.Next.Core; namespace Launcher.Next.Application; -public sealed record LegacyPluginDefinition(string Id, string Renewal, string Catalog, string Install, int DefinitionOrder); -public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList Plugins); +public sealed record LegacyPluginDefinition(string Id, string Renewal, string Catalog, string Install, int DefinitionOrder, IReadOnlyList? Machines = null) +{ + public bool IsAllowedOn(string machineName) => Machines is null || Machines.Count == 0 || Machines.Contains(machineName, StringComparer.OrdinalIgnoreCase); +} +public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList Plugins, IReadOnlyList? Attributes = null, IReadOnlyList? Sources = null, string? Target = null, string? Loader = null, string? ObjectPath = null); public sealed record LegacyLauncherSettings(string Help, string CareMarker, IReadOnlyList Patterns); public sealed record LegacyDownloadSelection(IReadOnlyList PluginIds); public sealed record LegacyCompatibilityMapping(string LegacyId, string PackageId, string Version, string Channel); @@ -27,8 +30,12 @@ public sealed class LegacyConfigurationAdapter plugin.Element("id")?.Value ?? throw new InvalidOperationException("LEGACY_PLUGIN_ID_MISSING"), plugin.Element("renewal")?.Value ?? throw new InvalidOperationException("LEGACY_RENEWAL_MISSING"), plugin.Element("catalog")?.Value ?? throw new InvalidOperationException("LEGACY_CATALOG_MISSING"), - plugin.Element("install")?.Value ?? throw new InvalidOperationException("LEGACY_INSTALL_MISSING"), index)).ToArray(); - return new LegacyProductPattern(Value("name"), Value("filter"), Value("roster"), plugins); + plugin.Element("install")?.Value ?? throw new InvalidOperationException("LEGACY_INSTALL_MISSING"), index, + plugin.Elements("machine").Select(x => x.Value.Trim()).Where(x => x.Length > 0).ToArray())).ToArray(); + return new LegacyProductPattern(Value("name"), Value("filter"), Value("roster"), plugins, + pattern.Elements("attrib").Select(x => x.Value).ToArray(), + pattern.Elements("source").Elements("course").Select(x => x.Value).ToArray(), + pattern.Element("target")?.Value, pattern.Element("loader")?.Value, pattern.Element("object")?.Value); }).ToArray(); return new(path.Element("help")?.Value ?? throw new InvalidOperationException("LEGACY_HELP_MISSING"), path.Element("care")?.Value ?? throw new InvalidOperationException("LEGACY_CARE_MISSING"), patterns); } diff --git a/Launcher.Next.Application/LegacyNetworkConfiguration.cs b/Launcher.Next.Application/LegacyNetworkConfiguration.cs new file mode 100644 index 0000000..4f6a523 --- /dev/null +++ b/Launcher.Next.Application/LegacyNetworkConfiguration.cs @@ -0,0 +1,61 @@ +using System.Security.Cryptography; + +namespace Launcher.Next.Application; + +public sealed record LegacySettingsSnapshot(string SourceEndpoint, string SourceFingerprint, string Sha256, long Length, DateTimeOffset LastWriteUtc, DateTimeOffset AcquiredUtc, string LocalSnapshotPath, bool OfflineFallback); + +public static class LegacyNetworkConfiguration +{ + public const string ApprovedSettingsEndpoint = @"\\phimproject.com\common\BIM\save\All_Serv_Install\Settings.xml"; + public const string LegacyBimAlias = @"N:\BIM"; + public const string ApprovedBimEndpoint = @"\\phimproject.com\common\BIM"; + + public static string ResolveApprovedAlias(string legacyPath) + { + if (string.IsNullOrWhiteSpace(legacyPath)) throw new InvalidOperationException("LEGACY_NETWORK_PATH_INVALID"); + var normalized = legacyPath.Replace('/', '\\'); + if (string.Equals(normalized, LegacyBimAlias, StringComparison.OrdinalIgnoreCase)) return ApprovedBimEndpoint; + var prefix = LegacyBimAlias + "\\"; + if (!normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_NETWORK_ALIAS_NOT_APPROVED"); + return ApprovedBimEndpoint + normalized[(prefix.Length - 1)..]; + } + + public static LegacySettingsSnapshot AcquireApprovedSnapshot(string endpoint, string localRoot) + { + if (!string.Equals(endpoint, ApprovedSettingsEndpoint, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_SETTINGS_ENDPOINT_NOT_APPROVED"); + if (!Path.IsPathFullyQualified(endpoint) || !endpoint.StartsWith(@"\\", StringComparison.Ordinal) || endpoint.Contains('?') || endpoint.Contains('*')) throw new InvalidOperationException("LEGACY_SETTINGS_ENDPOINT_INVALID"); + if (!File.Exists(endpoint) || File.GetAttributes(endpoint).HasFlag(FileAttributes.ReparsePoint)) return OfflineSnapshot(endpoint, localRoot); + var first = new FileInfo(endpoint); + var sourceHashBefore = Hash(endpoint); + var snapshots = Path.Combine(Path.GetFullPath(localRoot), "configuration-snapshots"); + Directory.CreateDirectory(snapshots); + var temporary = Path.Combine(snapshots, Guid.NewGuid().ToString("N") + ".tmp"); + File.Copy(endpoint, temporary, false); + var copiedHash = Hash(temporary); + var after = new FileInfo(endpoint); + var sourceHashAfter = Hash(endpoint); + if (first.Length != after.Length || first.LastWriteTimeUtc != after.LastWriteTimeUtc || !string.Equals(sourceHashBefore, sourceHashAfter, StringComparison.Ordinal) || !string.Equals(copiedHash, sourceHashAfter, StringComparison.Ordinal)) + { + File.Delete(temporary); + throw new InvalidOperationException("LEGACY_SETTINGS_CHANGED_DURING_ACQUISITION"); + } + var final = Path.Combine(snapshots, copiedHash + ".xml"); + if (File.Exists(final)) File.Delete(temporary); else File.Move(temporary, final); + if (!string.Equals(Hash(final), copiedHash, StringComparison.Ordinal)) throw new InvalidOperationException("LEGACY_SETTINGS_SNAPSHOT_HASH_MISMATCH"); + return new(endpoint, Fingerprint(endpoint, copiedHash), copiedHash, first.Length, new DateTimeOffset(first.LastWriteTimeUtc), DateTimeOffset.UtcNow, final, false); + } + + private static LegacySettingsSnapshot OfflineSnapshot(string endpoint, string localRoot) + { + var snapshots = Path.Combine(Path.GetFullPath(localRoot), "configuration-snapshots"); + if (!Directory.Exists(snapshots)) throw new InvalidOperationException("LEGACY_SETTINGS_NETWORK_UNAVAILABLE"); + var candidate = Directory.GetFiles(snapshots, "*.xml").OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault(); + if (candidate is null) throw new InvalidOperationException("LEGACY_SETTINGS_NETWORK_UNAVAILABLE"); + var info = new FileInfo(candidate); + var hash = Hash(candidate); + return new(endpoint, Fingerprint(endpoint, hash), hash, info.Length, new DateTimeOffset(info.LastWriteTimeUtc), DateTimeOffset.UtcNow, candidate, true); + } + + private static string Hash(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)); } + private static string Fingerprint(string endpoint, string hash) => Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(endpoint + "|" + hash))); +} diff --git a/scripts/Build-OwnerReviewDistribution.ps1 b/scripts/Build-OwnerReviewDistribution.ps1 index 22e6426..549158e 100644 --- a/scripts/Build-OwnerReviewDistribution.ps1 +++ b/scripts/Build-OwnerReviewDistribution.ps1 @@ -21,23 +21,20 @@ Copy-Item -LiteralPath (Join-Path $fixture 'project\Project.xml') -Destination ( $rosterDirectory = Join-Path $controlled '_System\Plant 3D_2019\Applications' New-Item -ItemType Directory -Path $rosterDirectory | Out-Null -Copy-Item -LiteralPath (Join-Path $fixture 'configuration\download_list.xml') -Destination (Join-Path $rosterDirectory 'download_list.xml') -Copy-Item -LiteralPath (Join-Path $fixture 'configuration\compatibility-overlay.json') -Destination (Join-Path $controlled 'configuration\compatibility-overlay.json') -@' - -Owner review controlled local PXP-OAowner-review-localPlant 3D_2019Project files |Project.xml|All files (*.*)|*.*%PROJECTDIRECTORY%\..\_System\Plant 3D_2019\Applications\download_list.xmlcontrolled-local-pxp-oaP_Rec_Annotlocal-immutable-package -'@ | Set-Content -LiteralPath (Join-Path $controlled 'configuration\Settings.xml') -Encoding utf8 +'b455ee6eaed79d76f084846a60ce3284' | Set-Content -LiteralPath (Join-Path $rosterDirectory 'download_list.xml') -Encoding utf8 +'{"schemaVersion":1,"mappings":[{"mappingId":"owner-network-p-rec-annot","pattern":"Plant 3D_2019","legacyPluginId":"b455ee6eaed79d76f084846a60ce3284","legacyCatalog":"P_Rec_Annot","legacyInstall":"OrthoLoader.dll","package":{"id":"P_Rec_Annot","product":"Plant3D","productYear":2026,"architecture":"x64","channel":"controlled-local","version":"0.1.0"}}]}' | Set-Content -LiteralPath (Join-Path $controlled 'configuration\compatibility-overlay.json') -Encoding utf8 $profile = [ordered]@{ schemaVersion = 1 name = 'Owner Review Controlled Local PXP-OA' root = $controlled - settingsPath = (Join-Path $controlled 'configuration\Settings.xml') + settingsPath = (Join-Path $controlled 'configuration\settings-pending.xml') downloadListPath = (Join-Path $rosterDirectory 'download_list.xml') overlayPath = (Join-Path $controlled 'configuration\compatibility-overlay.json') catalogRoot = (Join-Path $controlled 'catalog') patternName = 'Plant 3D_2019' evidenceRoot = (Join-Path $controlled 'evidence') + networkSettingsEndpoint = '\\phimproject.com\common\BIM\save\All_Serv_Install\Settings.xml' } $profilePath = Join-Path $controlled 'configuration\owner-review-profile.json' $profile | ConvertTo-Json | Set-Content -LiteralPath $profilePath -Encoding utf8 diff --git a/tests/Launcher.Next.Tests/LegacyNetworkConfigurationTests.cs b/tests/Launcher.Next.Tests/LegacyNetworkConfigurationTests.cs new file mode 100644 index 0000000..beb5787 --- /dev/null +++ b/tests/Launcher.Next.Tests/LegacyNetworkConfigurationTests.cs @@ -0,0 +1,39 @@ +using Launcher.Next.Application; +using Xunit; + +namespace Launcher.Next.Tests; + +public sealed class LegacyNetworkConfigurationTests +{ + [Fact] + public void ApprovedSettingsEndpoint_IsExactUncFile() + { + Assert.Equal(@"\\phimproject.com\common\BIM\save\All_Serv_Install\Settings.xml", LegacyNetworkConfiguration.ApprovedSettingsEndpoint); + Assert.StartsWith(@"\\", LegacyNetworkConfiguration.ApprovedSettingsEndpoint, StringComparison.Ordinal); + Assert.EndsWith("Settings.xml", LegacyNetworkConfiguration.ApprovedSettingsEndpoint, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData(@"N:\BIM", @"\\phimproject.com\common\BIM")] + [InlineData(@"N:\BIM\save\Apps\Plant3D\DriveA", @"\\phimproject.com\common\BIM\save\Apps\Plant3D\DriveA")] + public void ApprovedNDriveAlias_ResolvesOnlyBimNamespace(string original, string expected) + => Assert.Equal(expected, LegacyNetworkConfiguration.ResolveApprovedAlias(original)); + + [Theory] + [InlineData(@"N:\Other\payload.dll")] + [InlineData(@"Q:\BIM\save\Apps")] + [InlineData("relative\\Settings.xml")] + public void UnknownLegacyAliases_AreRejected(string path) + => Assert.Equal("LEGACY_NETWORK_ALIAS_NOT_APPROVED", Assert.Throws(() => LegacyNetworkConfiguration.ResolveApprovedAlias(path)).Message); + + [Fact] + public void UnapprovedEndpoint_IsRejectedBeforeAnyNetworkAccess() + { + var root = Path.Combine(Path.GetTempPath(), "launcher-network-" + Guid.NewGuid().ToString("N")); + try + { + Assert.Equal("LEGACY_SETTINGS_ENDPOINT_NOT_APPROVED", Assert.Throws(() => LegacyNetworkConfiguration.AcquireApprovedSnapshot(@"\\example.invalid\Settings.xml", root)).Message); + } + finally { if (Directory.Exists(root)) Directory.Delete(root, true); } + } +} diff --git a/tests/Launcher.Next.Tests/LegacyNetworkSettingsParserTests.cs b/tests/Launcher.Next.Tests/LegacyNetworkSettingsParserTests.cs new file mode 100644 index 0000000..6986276 --- /dev/null +++ b/tests/Launcher.Next.Tests/LegacyNetworkSettingsParserTests.cs @@ -0,0 +1,29 @@ +using Launcher.Next.Application; +using Xunit; + +namespace Launcher.Next.Tests; + +public sealed class LegacyNetworkSettingsParserTests +{ + [Fact] + public void FullLegacyPattern_PreservesSourcesAttributesAndMachineRestriction() + { + const string xml = "hcPlant 3D_2019Project files |Project.xml|All files (*.*)|*.*%PROJECTDIRECTORY%\\..\\_System\\Plant 3D_2019\\Applications\\download_list.xmlN:\\BIM\\save\\Apps\\Plant3D\\DriveAN:\\BIM\\save\\Apps\\Plant3D\\DriveQtargetloaderobjectfirstsecondb455ee6eaed79d76f084846a60ce3284P_Rec_AnnotOrthoLoader.dllBUILD-01"; + var settings = new LegacyConfigurationAdapter().ParseSettings(xml); + var pattern = Assert.Single(settings.Patterns); + Assert.Equal(["N:\\BIM\\save\\Apps\\Plant3D\\DriveA", "N:\\BIM\\save\\Apps\\Plant3D\\DriveQ"], pattern.Sources); + Assert.Equal(["first", "second"], pattern.Attributes); + Assert.Equal("target", pattern.Target); + var plugin = Assert.Single(pattern.Plugins); + Assert.Equal("P_Rec_Annot", plugin.Catalog); + Assert.True(plugin.IsAllowedOn("build-01")); + Assert.False(plugin.IsAllowedOn("OTHER")); + } + + [Fact] + public void PluginWithoutMachineRestriction_IsAllowedEverywhere() + { + var plugin = new LegacyPluginDefinition("id", "", "catalog", "install", 0); + Assert.True(plugin.IsAllowedOn(Environment.MachineName)); + } +}