feat: enforce resolved catalog path roles
This commit is contained in:
@@ -20,10 +20,19 @@ public sealed record ResolvedLauncherPaths(IReadOnlyDictionary<string, string?>
|
|||||||
public string? FixtureOrthoDwg => Get(nameof(FixtureOrthoDwg));
|
public string? FixtureOrthoDwg => Get(nameof(FixtureOrthoDwg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Catalog source role resolved by <see cref="IPathResolver"/>. Raw
|
||||||
|
/// filesystem strings are intentionally not accepted by catalog acquisition.</summary>
|
||||||
|
public sealed record ResolvedCatalogRoot
|
||||||
|
{
|
||||||
|
internal ResolvedCatalogRoot(string value) => Value = value;
|
||||||
|
public string Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
public interface IPathResolver
|
public interface IPathResolver
|
||||||
{
|
{
|
||||||
ResolvedLauncherPaths Resolve(LauncherPathVariables variables, IEnumerable<string>? required = null);
|
ResolvedLauncherPaths Resolve(LauncherPathVariables variables, IEnumerable<string>? required = null);
|
||||||
string ResolvePath(string value, LauncherPathVariables variables, string? constrainedRoot = null);
|
string ResolvePath(string value, LauncherPathVariables variables, string? constrainedRoot = null);
|
||||||
|
ResolvedCatalogRoot ResolveCatalogRoot(LauncherPathVariables variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environment = null) : IPathResolver
|
public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environment = null) : IPathResolver
|
||||||
@@ -74,6 +83,16 @@ public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environme
|
|||||||
return full;
|
return full;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ResolvedCatalogRoot ResolveCatalogRoot(LauncherPathVariables variables)
|
||||||
|
{
|
||||||
|
var resolved = Resolve(variables, [LauncherPathVariableNames.CorporateCatalogRoot, LauncherPathVariableNames.CorporateDfsRoot]);
|
||||||
|
var catalog = ResolvePath(resolved.Require(LauncherPathVariableNames.CorporateCatalogRoot), variables);
|
||||||
|
var corporate = ResolvePath(resolved.Require(LauncherPathVariableNames.CorporateDfsRoot), variables);
|
||||||
|
if (!catalog.StartsWith(corporate.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException("PATH_ROLE_COLLISION");
|
||||||
|
return new ResolvedCatalogRoot(catalog);
|
||||||
|
}
|
||||||
|
|
||||||
private string? ResolveVariable(string name, LauncherPathVariables variables, Dictionary<string, string?> resolved, HashSet<string> stack)
|
private string? ResolveVariable(string name, LauncherPathVariables variables, Dictionary<string, string?> resolved, HashSet<string> stack)
|
||||||
{
|
{
|
||||||
if (resolved.TryGetValue(name, out var cached)) return cached;
|
if (resolved.TryGetValue(name, out var cached)) return cached;
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ namespace Launcher.Next.Infrastructure;
|
|||||||
/// local immutable staging by <see cref="IPackageSynchronizer"/>; it is never
|
/// local immutable staging by <see cref="IPackageSynchronizer"/>; it is never
|
||||||
/// an active launch root.
|
/// an active launch root.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FileSystemCatalogPackageAcquirer(string catalogRoot) : IPackageCatalogPackageAcquirer
|
public sealed class FileSystemCatalogPackageAcquirer(ResolvedCatalogRoot catalogRoot) : IPackageCatalogPackageAcquirer
|
||||||
{
|
{
|
||||||
private const int MaxFiles = 256;
|
private const int MaxFiles = 256;
|
||||||
private const long MaxBytes = 512L * 1024 * 1024;
|
private const long MaxBytes = 512L * 1024 * 1024;
|
||||||
|
|
||||||
public async Task<CatalogPackage?> AcquireAsync(PackageRequirement requirement, CancellationToken cancellationToken)
|
public async Task<CatalogPackage?> AcquireAsync(PackageRequirement requirement, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var root = Path.GetFullPath(catalogRoot);
|
var root = catalogRoot.Value;
|
||||||
var candidate = Path.GetFullPath(Path.Combine(root, requirement.PackageId, requirement.RequiredVersion));
|
var candidate = Path.GetFullPath(Path.Combine(root, requirement.PackageId, requirement.RequiredVersion));
|
||||||
if (!Directory.Exists(root) || IsReparsePoint(root) || !IsWithin(root, candidate) || !Directory.Exists(candidate) || IsReparsePoint(candidate))
|
if (!Directory.Exists(root) || IsReparsePoint(root) || !IsWithin(root, candidate) || !Directory.Exists(candidate) || IsReparsePoint(candidate))
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public sealed class RosterAdaptersTests
|
|||||||
var bytes=new byte[]{1}; await File.WriteAllBytesAsync(Path.Combine(packageRoot,"entry.dll"),bytes);
|
var bytes=new byte[]{1}; await File.WriteAllBytesAsync(Path.Combine(packageRoot,"entry.dll"),bytes);
|
||||||
var hash=Convert.ToHexString(SHA256.HashData(bytes));
|
var hash=Convert.ToHexString(SHA256.HashData(bytes));
|
||||||
await File.WriteAllTextAsync(Path.Combine(packageRoot,"package.manifest.json"), $$"""{"schemaVersion":1,"packageId":"P_A","version":"1.0.0","product":"Plant3D","productYear":2026,"architecture":"x64","entryAssembly":"entry.dll","files":[{"path":"entry.dll","length":1,"sha256":"{{hash}}"}],"startup":{"type":"netload","command":"P_A"},"rolloutChannel":"stable"}""");
|
await File.WriteAllTextAsync(Path.Combine(packageRoot,"package.manifest.json"), $$"""{"schemaVersion":1,"packageId":"P_A","version":"1.0.0","product":"Plant3D","productYear":2026,"architecture":"x64","entryAssembly":"entry.dll","files":[{"path":"entry.dll","length":1,"sha256":"{{hash}}"}],"startup":{"type":"netload","command":"P_A"},"rolloutChannel":"stable"}""");
|
||||||
var result=await new FileSystemCatalogPackageAcquirer(root).AcquireAsync(new("P_A","Plant3D",2026,"x64","stable","1.0.0","project-a",true),CancellationToken.None);
|
var result=await new FileSystemCatalogPackageAcquirer(CatalogRoot(root)).AcquireAsync(new("P_A","Plant3D",2026,"x64","stable","1.0.0","project-a",true),CancellationToken.None);
|
||||||
Assert.NotNull(result); Assert.Equal(packageRoot,result!.SourceRoot); Assert.False(result.SourceRoot.StartsWith(@"\\",StringComparison.Ordinal));
|
Assert.NotNull(result); Assert.Equal(packageRoot,result!.SourceRoot); Assert.False(result.SourceRoot.StartsWith(@"\\",StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
finally { Directory.Delete(root,true); }
|
finally { Directory.Delete(root,true); }
|
||||||
@@ -89,10 +89,11 @@ public sealed class RosterAdaptersTests
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await File.WriteAllTextAsync(Path.Combine(packageRoot,"package.manifest.json"), """{"schemaVersion":1,"packageId":"P_A","version":"1.0.0","product":"Plant3D","productYear":2026,"architecture":"x64","entryAssembly":"entry.dll","files":[{"path":"../outside.dll","length":1,"sha256":"00"}],"startup":{"type":"netload","command":"P_A"},"rolloutChannel":"stable"}""");
|
await File.WriteAllTextAsync(Path.Combine(packageRoot,"package.manifest.json"), """{"schemaVersion":1,"packageId":"P_A","version":"1.0.0","product":"Plant3D","productYear":2026,"architecture":"x64","entryAssembly":"entry.dll","files":[{"path":"../outside.dll","length":1,"sha256":"00"}],"startup":{"type":"netload","command":"P_A"},"rolloutChannel":"stable"}""");
|
||||||
Assert.Null(await new FileSystemCatalogPackageAcquirer(root).AcquireAsync(new("P_A","Plant3D",2026,"x64","stable","1.0.0","project-a",true),CancellationToken.None));
|
Assert.Null(await new FileSystemCatalogPackageAcquirer(CatalogRoot(root)).AcquireAsync(new("P_A","Plant3D",2026,"x64","stable","1.0.0","project-a",true),CancellationToken.None));
|
||||||
}
|
}
|
||||||
finally { Directory.Delete(root,true); }
|
finally { Directory.Delete(root,true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Json() => """{"schemaVersion":1,"rosterId":"project-a","product":"Plant3D","year":2026,"architecture":"x64","offlinePolicy":"AllowVerifiedLastKnownGood","packages":[{"id":"P_B","version":"2.0.0","channel":"stable","loadOrder":200},{"id":"P_A","version":"1.0.0","channel":"stable","loadOrder":100}]}""";
|
private static string Json() => """{"schemaVersion":1,"rosterId":"project-a","product":"Plant3D","year":2026,"architecture":"x64","offlinePolicy":"AllowVerifiedLastKnownGood","packages":[{"id":"P_B","version":"2.0.0","channel":"stable","loadOrder":200},{"id":"P_A","version":"1.0.0","channel":"stable","loadOrder":100}]}""";
|
||||||
|
private static ResolvedCatalogRoot CatalogRoot(string root) => new PathResolver().ResolveCatalogRoot(new(new Dictionary<string,string?> { [LauncherPathVariableNames.CorporateDfsRoot] = Path.GetDirectoryName(root)!, [LauncherPathVariableNames.CorporateCatalogRoot] = root }));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user