fix: canonicalize legacy drive aliases in resolved paths

This commit is contained in:
codex
2026-08-04 11:50:14 +03:00
parent e13f44ac3c
commit c1587b7f66
2 changed files with 11 additions and 4 deletions
+10 -4
View File
@@ -36,6 +36,12 @@ public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environme
{ {
var resolved = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); var resolved = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
foreach (var name in variables.Values.Keys) resolved[name] = ResolveVariable(name, variables, resolved, []); 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 ?? []) foreach (var name in required ?? [])
if (!resolved.TryGetValue(name, out var value) || string.IsNullOrWhiteSpace(value)) if (!resolved.TryGetValue(name, out var value) || string.IsNullOrWhiteSpace(value))
throw new InvalidOperationException($"PATH_VARIABLE_REQUIRED:{name}"); throw new InvalidOperationException($"PATH_VARIABLE_REQUIRED:{name}");
@@ -47,12 +53,12 @@ public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environme
var resolved = Expand(value, variables, new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase), []); var resolved = Expand(value, variables, new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase), []);
if (string.IsNullOrWhiteSpace(resolved) || !Path.IsPathFullyQualified(resolved)) if (string.IsNullOrWhiteSpace(resolved) || !Path.IsPathFullyQualified(resolved))
throw new InvalidOperationException("PATH_ABSOLUTE_REQUIRED"); throw new InvalidOperationException("PATH_ABSOLUTE_REQUIRED");
var full = Path.GetFullPath(resolved); if (resolved.StartsWith("A:\\", StringComparison.OrdinalIgnoreCase))
if (full.StartsWith("A:\\", StringComparison.OrdinalIgnoreCase))
{ {
var corporate = Resolve(variables, ["CorporateDfsRoot"]).Require("CorporateDfsRoot"); var corporate = Resolve(variables, [LauncherPathVariableNames.CorporateDfsRoot]).Require(LauncherPathVariableNames.CorporateDfsRoot);
full = Path.Combine(corporate, full[3..]); 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 (full.StartsWith("A:\\", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_DRIVE_A_UNRESOLVED");
if (!string.IsNullOrWhiteSpace(constrainedRoot)) if (!string.IsNullOrWhiteSpace(constrainedRoot))
{ {
@@ -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_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_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_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<string, string?> { [LauncherPathVariableNames.CorporateDfsRoot] = "\\\\phimproject.com\\a", ["Legacy"] = "A:\\Project" })).Require("Legacy").StartsWith("A:\\", StringComparison.OrdinalIgnoreCase));
[Fact] public void PathResolver_RejectsTraversalOutsideRoot() => Assert.Throws<InvalidOperationException>(() => Resolver().ResolvePath("${LocalSessionRoot}\\..\\escape", new(Paths), "${LocalSessionRoot}")); [Fact] public void PathResolver_RejectsTraversalOutsideRoot() => Assert.Throws<InvalidOperationException>(() => Resolver().ResolvePath("${LocalSessionRoot}\\..\\escape", new(Paths), "${LocalSessionRoot}"));
[Fact] public void PathResolver_RequiresAbsolutePath() => Assert.Throws<InvalidOperationException>(() => Resolver().ResolvePath("relative\\file", new(Paths))); [Fact] public void PathResolver_RequiresAbsolutePath() => Assert.Throws<InvalidOperationException>(() => Resolver().ResolvePath("relative\\file", new(Paths)));
[Fact] public void ProjectRoot_UsesProjectAndDisciplineVariables() => Assert.Equal("\\\\phimproject.com\\a\\Проект", Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.ProjectRoot)); [Fact] public void ProjectRoot_UsesProjectAndDisciplineVariables() => Assert.Equal("\\\\phimproject.com\\a\\Проект", Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.ProjectRoot));