From c1587b7f667c78c57f1c49de1c10e3d29172c207 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 4 Aug 2026 11:50:14 +0300 Subject: [PATCH] fix: canonicalize legacy drive aliases in resolved paths --- Launcher.Next.Core/LauncherPaths.cs | 14 ++++++++++---- tests/Launcher.Next.Tests/PathAndLaunchTests.cs | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Launcher.Next.Core/LauncherPaths.cs b/Launcher.Next.Core/LauncherPaths.cs index 5c7d9e1..a09bdb5 100644 --- a/Launcher.Next.Core/LauncherPaths.cs +++ b/Launcher.Next.Core/LauncherPaths.cs @@ -36,6 +36,12 @@ public sealed class PathResolver(IReadOnlyDictionary? environme { var resolved = new Dictionary(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}"); @@ -47,12 +53,12 @@ public sealed class PathResolver(IReadOnlyDictionary? environme var resolved = Expand(value, variables, new Dictionary(StringComparer.OrdinalIgnoreCase), []); if (string.IsNullOrWhiteSpace(resolved) || !Path.IsPathFullyQualified(resolved)) throw new InvalidOperationException("PATH_ABSOLUTE_REQUIRED"); - var full = Path.GetFullPath(resolved); - if (full.StartsWith("A:\\", StringComparison.OrdinalIgnoreCase)) + if (resolved.StartsWith("A:\\", StringComparison.OrdinalIgnoreCase)) { - var corporate = Resolve(variables, ["CorporateDfsRoot"]).Require("CorporateDfsRoot"); - full = Path.Combine(corporate, full[3..]); + 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)) { diff --git a/tests/Launcher.Next.Tests/PathAndLaunchTests.cs b/tests/Launcher.Next.Tests/PathAndLaunchTests.cs index e80a035..ff7e99e 100644 --- a/tests/Launcher.Next.Tests/PathAndLaunchTests.cs +++ b/tests/Launcher.Next.Tests/PathAndLaunchTests.cs @@ -32,6 +32,7 @@ public sealed class PathAndLaunchTests [Fact] public void PathResolver_NormalizesSeparators() => Assert.DoesNotContain('/', Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.DisciplineRoot)); [Fact] public void PathResolver_NormalizesLegacyDriveAToCorporateDfs() => Assert.Equal("\\\\phimproject.com\\a\\Project\\Plant", Resolver().ResolvePath("A:\\Project\\Plant", new(Paths))); [Fact] public void PathResolver_DoesNotEmitDriveA() => Assert.False(Resolver().ResolvePath("A:\\Project", new(Paths)).StartsWith("A:\\", StringComparison.OrdinalIgnoreCase)); + [Fact] public void PathResolver_CanonicalVariablesDoNotEmitDriveA() => Assert.False(Resolver().Resolve(new(new Dictionary { [LauncherPathVariableNames.CorporateDfsRoot] = "\\\\phimproject.com\\a", ["Legacy"] = "A:\\Project" })).Require("Legacy").StartsWith("A:\\", StringComparison.OrdinalIgnoreCase)); [Fact] public void PathResolver_RejectsTraversalOutsideRoot() => Assert.Throws(() => Resolver().ResolvePath("${LocalSessionRoot}\\..\\escape", new(Paths), "${LocalSessionRoot}")); [Fact] public void PathResolver_RequiresAbsolutePath() => Assert.Throws(() => Resolver().ResolvePath("relative\\file", new(Paths))); [Fact] public void ProjectRoot_UsesProjectAndDisciplineVariables() => Assert.Equal("\\\\phimproject.com\\a\\Проект", Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.ProjectRoot));