feat: acquire legacy settings from approved network source

This commit is contained in:
codex
2026-08-06 13:56:54 +03:00
parent 0213b229a9
commit 97fbc1d746
6 changed files with 153 additions and 13 deletions
@@ -21,7 +21,8 @@ namespace Launcher.Next.App;
/// </summary> /// </summary>
internal sealed record LocalParityLaunchProfile( internal sealed record LocalParityLaunchProfile(
int SchemaVersion, string Name, string Root, string SettingsPath, string DownloadListPath, 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, internal sealed record LocalParityPrepared(LocalParityLaunchProfile Profile, PackageRequirement Requirement,
PackageManifest Manifest, ImmutableLocalPackage Package, PackageActivationResult Activation, 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)) if (profile.SchemaVersion != 1 || string.IsNullOrWhiteSpace(profile.Name) || string.IsNullOrWhiteSpace(profile.PatternName))
throw new InvalidOperationException("LOCAL_PARITY_PROFILE_INVALID"); throw new InvalidOperationException("LOCAL_PARITY_PROFILE_INVALID");
ValidateProfile(profile); 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<LocalParityPrepared> PrepareAsync(LocalParityLaunchProfile profile, CancellationToken ct) public async Task<LocalParityPrepared> PrepareAsync(LocalParityLaunchProfile profile, CancellationToken ct)
@@ -4,8 +4,11 @@ using Launcher.Next.Core;
namespace Launcher.Next.Application; namespace Launcher.Next.Application;
public sealed record LegacyPluginDefinition(string Id, string Renewal, string Catalog, string Install, int DefinitionOrder); public sealed record LegacyPluginDefinition(string Id, string Renewal, string Catalog, string Install, int DefinitionOrder, IReadOnlyList<string>? Machines = null)
public sealed record LegacyProductPattern(string Name, string Filter, string Roster, IReadOnlyList<LegacyPluginDefinition> Plugins); {
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<LegacyPluginDefinition> Plugins, IReadOnlyList<string>? Attributes = null, IReadOnlyList<string>? Sources = null, string? Target = null, string? Loader = null, string? ObjectPath = null);
public sealed record LegacyLauncherSettings(string Help, string CareMarker, IReadOnlyList<LegacyProductPattern> Patterns); public sealed record LegacyLauncherSettings(string Help, string CareMarker, IReadOnlyList<LegacyProductPattern> Patterns);
public sealed record LegacyDownloadSelection(IReadOnlyList<string> PluginIds); public sealed record LegacyDownloadSelection(IReadOnlyList<string> PluginIds);
public sealed record LegacyCompatibilityMapping(string LegacyId, string PackageId, string Version, string Channel); 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("id")?.Value ?? throw new InvalidOperationException("LEGACY_PLUGIN_ID_MISSING"),
plugin.Element("renewal")?.Value ?? throw new InvalidOperationException("LEGACY_RENEWAL_MISSING"), plugin.Element("renewal")?.Value ?? throw new InvalidOperationException("LEGACY_RENEWAL_MISSING"),
plugin.Element("catalog")?.Value ?? throw new InvalidOperationException("LEGACY_CATALOG_MISSING"), plugin.Element("catalog")?.Value ?? throw new InvalidOperationException("LEGACY_CATALOG_MISSING"),
plugin.Element("install")?.Value ?? throw new InvalidOperationException("LEGACY_INSTALL_MISSING"), index)).ToArray(); plugin.Element("install")?.Value ?? throw new InvalidOperationException("LEGACY_INSTALL_MISSING"), index,
return new LegacyProductPattern(Value("name"), Value("filter"), Value("roster"), plugins); 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(); }).ToArray();
return new(path.Element("help")?.Value ?? throw new InvalidOperationException("LEGACY_HELP_MISSING"), path.Element("care")?.Value ?? throw new InvalidOperationException("LEGACY_CARE_MISSING"), patterns); return new(path.Element("help")?.Value ?? throw new InvalidOperationException("LEGACY_HELP_MISSING"), path.Element("care")?.Value ?? throw new InvalidOperationException("LEGACY_CARE_MISSING"), patterns);
} }
@@ -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)));
}
+4 -7
View File
@@ -21,23 +21,20 @@ Copy-Item -LiteralPath (Join-Path $fixture 'project\Project.xml') -Destination (
$rosterDirectory = Join-Path $controlled '_System\Plant 3D_2019\Applications' $rosterDirectory = Join-Path $controlled '_System\Plant 3D_2019\Applications'
New-Item -ItemType Directory -Path $rosterDirectory | Out-Null 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') '<root><id>b455ee6eaed79d76f084846a60ce3284</id></root>' | Set-Content -LiteralPath (Join-Path $rosterDirectory 'download_list.xml') -Encoding utf8
Copy-Item -LiteralPath (Join-Path $fixture 'configuration\compatibility-overlay.json') -Destination (Join-Path $controlled 'configuration\compatibility-overlay.json') '{"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
@'
<?xml version="1.0" encoding="utf-8"?>
<root><path><help>Owner review controlled local PXP-OA</help><care>owner-review-local</care></path><data><pattern><name>Plant 3D_2019</name><filter>Project files |Project.xml|All files (*.*)|*.*</filter><roster>%PROJECTDIRECTORY%\..\_System\Plant 3D_2019\Applications\download_list.xml</roster></pattern><plugin><id>controlled-local-pxp-oa</id><renewal></renewal><catalog>P_Rec_Annot</catalog><install>local-immutable-package</install></plugin></data></root>
'@ | Set-Content -LiteralPath (Join-Path $controlled 'configuration\Settings.xml') -Encoding utf8
$profile = [ordered]@{ $profile = [ordered]@{
schemaVersion = 1 schemaVersion = 1
name = 'Owner Review Controlled Local PXP-OA' name = 'Owner Review Controlled Local PXP-OA'
root = $controlled root = $controlled
settingsPath = (Join-Path $controlled 'configuration\Settings.xml') settingsPath = (Join-Path $controlled 'configuration\settings-pending.xml')
downloadListPath = (Join-Path $rosterDirectory 'download_list.xml') downloadListPath = (Join-Path $rosterDirectory 'download_list.xml')
overlayPath = (Join-Path $controlled 'configuration\compatibility-overlay.json') overlayPath = (Join-Path $controlled 'configuration\compatibility-overlay.json')
catalogRoot = (Join-Path $controlled 'catalog') catalogRoot = (Join-Path $controlled 'catalog')
patternName = 'Plant 3D_2019' patternName = 'Plant 3D_2019'
evidenceRoot = (Join-Path $controlled 'evidence') 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' $profilePath = Join-Path $controlled 'configuration\owner-review-profile.json'
$profile | ConvertTo-Json | Set-Content -LiteralPath $profilePath -Encoding utf8 $profile | ConvertTo-Json | Set-Content -LiteralPath $profilePath -Encoding utf8
@@ -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<InvalidOperationException>(() => 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<InvalidOperationException>(() => LegacyNetworkConfiguration.AcquireApprovedSnapshot(@"\\example.invalid\Settings.xml", root)).Message);
}
finally { if (Directory.Exists(root)) Directory.Delete(root, true); }
}
}
@@ -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 = "<root><path><help>h</help><care>c</care></path><data><pattern><name>Plant 3D_2019</name><filter>Project files |Project.xml|All files (*.*)|*.*</filter><roster>%PROJECTDIRECTORY%\\..\\_System\\Plant 3D_2019\\Applications\\download_list.xml</roster><source><course>N:\\BIM\\save\\Apps\\Plant3D\\DriveA</course></source><source><course>N:\\BIM\\save\\Apps\\Plant3D\\DriveQ</course></source><target>target</target><loader>loader</loader><object>object</object><attrib>first</attrib><attrib>second</attrib></pattern><plugin><id>b455ee6eaed79d76f084846a60ce3284</id><renewal></renewal><catalog>P_Rec_Annot</catalog><install>OrthoLoader.dll</install><machine>BUILD-01</machine></plugin></data></root>";
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));
}
}