feat: acquire legacy settings from approved network source
This commit is contained in:
@@ -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)));
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user