From 66e46db8493815343e5d6fafac7b7cadeac52410 Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 6 Aug 2026 09:46:50 +0300 Subject: [PATCH] feat: add typed local package pilot root --- Launcher.Next.App/PackageLifecyclePilotCli.cs | 5 +++-- Launcher.Next.Core/LauncherPaths.cs | 9 +++++++++ tests/Launcher.Next.Tests/PackageLifecycleTests.cs | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Launcher.Next.App/PackageLifecyclePilotCli.cs b/Launcher.Next.App/PackageLifecyclePilotCli.cs index 7b77719..d3a8bb9 100644 --- a/Launcher.Next.App/PackageLifecyclePilotCli.cs +++ b/Launcher.Next.App/PackageLifecyclePilotCli.cs @@ -10,8 +10,9 @@ internal static class PackageLifecyclePilotCli { public static async Task RunAsync(string[] args) { - if (args.Length != 4 || args[0] != "--mode" || args[1] != "PackageLifecyclePilot" || args[2] != "--root" || !Path.IsPathFullyQualified(args[3]) || args[3].StartsWith(@"\\")) return 2; - var root=Path.GetFullPath(args[3]); var id=Guid.NewGuid().ToString("N"); var evidenceDir=Path.Combine(root,"evidence",id); Directory.CreateDirectory(evidenceDir); + if (args.Length != 4 || args[0] != "--mode" || args[1] != "PackageLifecyclePilot" || args[2] != "--root") return 2; + ResolvedPackagePilotRoot pilotRoot; try { pilotRoot=new PathResolver().ResolvePackagePilotRoot(args[3]); } catch { return 2; } + var root=pilotRoot.Value; var id=Guid.NewGuid().ToString("N"); var evidenceDir=Path.Combine(root,"evidence",id); Directory.CreateDirectory(evidenceDir); try { var catalog=Path.Combine(root,"catalog"); Directory.CreateDirectory(catalog); var store=new LocalPackageStore(root,new Sha256FileHasher()); diff --git a/Launcher.Next.Core/LauncherPaths.cs b/Launcher.Next.Core/LauncherPaths.cs index b478179..52a8c51 100644 --- a/Launcher.Next.Core/LauncherPaths.cs +++ b/Launcher.Next.Core/LauncherPaths.cs @@ -31,6 +31,12 @@ public sealed record ResolvedLegacySettingsPath { internal ResolvedLegacySetting 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 interface IPathResolver { @@ -41,6 +47,7 @@ public interface IPathResolver ResolvedLegacyDownloadListPath ResolveLegacyDownloadListPath(LauncherPathVariables variables); ResolvedLegacyCompatibilityOverlayPath ResolveLegacyCompatibilityOverlayPath(LauncherPathVariables variables); ResolvedLegacyProbeEvidenceRoot ResolveLegacyProbeEvidenceRoot(LauncherPathVariables variables); + ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value); } public sealed class PathResolver(IReadOnlyDictionary? environment = null) : IPathResolver @@ -109,6 +116,8 @@ public sealed class PathResolver(IReadOnlyDictionary? environme 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)||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); } private string ResolveExistingFile(string role, LauncherPathVariables variables) { var value=ResolvePath(Resolve(variables,[role]).Require(role),variables); diff --git a/tests/Launcher.Next.Tests/PackageLifecycleTests.cs b/tests/Launcher.Next.Tests/PackageLifecycleTests.cs index 4d00ff3..09cc096 100644 --- a/tests/Launcher.Next.Tests/PackageLifecycleTests.cs +++ b/tests/Launcher.Next.Tests/PackageLifecycleTests.cs @@ -7,6 +7,8 @@ namespace Launcher.Next.Tests; public sealed class PackageLifecycleTests { + [Fact] public void PilotRootRejectsUncAndRelativePaths() + { var resolver=new PathResolver(new Dictionary()); Assert.Throws(()=>resolver.ResolvePackagePilotRoot(@"\\server\share")); Assert.Throws(()=>resolver.ResolvePackagePilotRoot("relative")); } private static PackageRequirement Req(string version) => new("Lifecycle.Package", "Plant3D", 2026, "x64", "pilot", version, "lifecycle-test", true); private static PackageManifest Manifest(string version, byte[] a, byte[] b) => new(1, "Lifecycle.Package", version, "Plant3D", 2026, "x64", "entry.dll", [ new("entry.dll", a.Length, Convert.ToHexString(SHA256.HashData(a))),