mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-14 06:34:30 +08:00
fix(config): 修复Godot YAML配置加载器的目录重置异常处理
- 为构造函数添加ArgumentNullException和ArgumentException异常说明 - 为EnableHotReload方法添加InvalidOperationException异常说明 - 重构ResetDirectory方法以捕获目录操作异常并包装为ConfigLoadException - 添加detail参数到CreateConfigLoadException方法用于提供更详细的错误信息 - 新增单元测试验证运行时缓存目录重置失败时的异常处理 - 添加GodotYamlConfigTableSourceTests测试类验证安全相对路径约束
This commit is contained in:
parent
e746297496
commit
1bf5d287e9
@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
using GFramework.Game.Config;
|
using GFramework.Game.Config;
|
||||||
using GFramework.Godot.Config;
|
using GFramework.Godot.Config;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -176,6 +177,31 @@ public sealed class GodotYamlConfigLoaderTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证运行时缓存目录无法重置时,Godot 适配层仍会返回结构化的配置加载诊断。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Wrap_Runtime_Cache_Directory_Reset_Failure_As_ConfigLoadException()
|
||||||
|
{
|
||||||
|
CreateMonsterFiles(_resourceRoot);
|
||||||
|
WriteFile(_userRoot, "config_cache", "occupied");
|
||||||
|
|
||||||
|
var loader = CreateLoader(isEditor: false);
|
||||||
|
|
||||||
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () =>
|
||||||
|
await loader.LoadAsync(new ConfigRegistry()));
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ConfigFileReadFailed));
|
||||||
|
Assert.That(exception.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||||
|
Assert.That(exception.Diagnostic.ConfigDirectoryPath, Is.EqualTo(Path.Combine(_resourceRoot, "monster")));
|
||||||
|
Assert.That(exception.Diagnostic.Detail, Does.Contain(Path.Combine(_userRoot, "config_cache", "monster")));
|
||||||
|
Assert.That(exception.InnerException, Is.InstanceOf<IOException>());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证加载器自身会拒绝可能逃逸缓存根目录的非法配置目录路径,即使调用方绕过了公开构造约束。
|
/// 验证加载器自身会拒绝可能逃逸缓存根目录的非法配置目录路径,即使调用方绕过了公开构造约束。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -15,11 +15,16 @@ public sealed class GodotYamlConfigTableSourceTests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configRelativePath">待验证的配置目录路径。</param>
|
/// <param name="configRelativePath">待验证的配置目录路径。</param>
|
||||||
[TestCase("../outside")]
|
[TestCase("../outside")]
|
||||||
|
[TestCase(@"..\outside")]
|
||||||
[TestCase("./monster")]
|
[TestCase("./monster")]
|
||||||
|
[TestCase(@".\monster")]
|
||||||
[TestCase("monster/../outside")]
|
[TestCase("monster/../outside")]
|
||||||
|
[TestCase(@"monster\..\outside")]
|
||||||
[TestCase("monster/./child")]
|
[TestCase("monster/./child")]
|
||||||
|
[TestCase(@"monster\.\child")]
|
||||||
[TestCase("/monster")]
|
[TestCase("/monster")]
|
||||||
[TestCase("C:/monster")]
|
[TestCase("C:/monster")]
|
||||||
|
[TestCase(@"C:\monster")]
|
||||||
[TestCase("res://monster")]
|
[TestCase("res://monster")]
|
||||||
[TestCase("user://monster")]
|
[TestCase("user://monster")]
|
||||||
public void Constructor_Should_Throw_When_Config_Relative_Path_Is_Not_Safe(string configRelativePath)
|
public void Constructor_Should_Throw_When_Config_Relative_Path_Is_Not_Safe(string configRelativePath)
|
||||||
@ -35,11 +40,16 @@ public sealed class GodotYamlConfigTableSourceTests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="schemaRelativePath">待验证的 schema 路径。</param>
|
/// <param name="schemaRelativePath">待验证的 schema 路径。</param>
|
||||||
[TestCase("../schemas/monster.schema.json")]
|
[TestCase("../schemas/monster.schema.json")]
|
||||||
|
[TestCase(@"..\schemas\monster.schema.json")]
|
||||||
[TestCase("./schemas/monster.schema.json")]
|
[TestCase("./schemas/monster.schema.json")]
|
||||||
|
[TestCase(@".\schemas\monster.schema.json")]
|
||||||
[TestCase("schemas/../monster.schema.json")]
|
[TestCase("schemas/../monster.schema.json")]
|
||||||
|
[TestCase(@"schemas\..\monster.schema.json")]
|
||||||
[TestCase("schemas/./monster.schema.json")]
|
[TestCase("schemas/./monster.schema.json")]
|
||||||
|
[TestCase(@"schemas\.\monster.schema.json")]
|
||||||
[TestCase("/schemas/monster.schema.json")]
|
[TestCase("/schemas/monster.schema.json")]
|
||||||
[TestCase("C:/schemas/monster.schema.json")]
|
[TestCase("C:/schemas/monster.schema.json")]
|
||||||
|
[TestCase(@"C:\schemas\monster.schema.json")]
|
||||||
[TestCase("res://schemas/monster.schema.json")]
|
[TestCase("res://schemas/monster.schema.json")]
|
||||||
[TestCase("user://schemas/monster.schema.json")]
|
[TestCase("user://schemas/monster.schema.json")]
|
||||||
public void Constructor_Should_Throw_When_Schema_Relative_Path_Is_Not_Safe(string schemaRelativePath)
|
public void Constructor_Should_Throw_When_Schema_Relative_Path_Is_Not_Safe(string schemaRelativePath)
|
||||||
|
|||||||
@ -22,6 +22,20 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
/// 使用指定选项创建一个 Godot YAML 配置加载器。
|
/// 使用指定选项创建一个 Godot YAML 配置加载器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options">加载器初始化选项。</param>
|
/// <param name="options">加载器初始化选项。</param>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="options" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||||
|
/// <exception cref="ArgumentException">
|
||||||
|
/// 当 <see cref="GodotYamlConfigLoaderOptions.SourceRootPath" /> 或
|
||||||
|
/// <see cref="GodotYamlConfigLoaderOptions.RuntimeCacheRootPath" /> 为空白字符串时抛出。
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// 当 Godot 特殊路径无法被全局化为非空绝对路径时抛出。
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// 构造完成后,加载器会根据当前环境决定直接读取 <see cref="SourceRootPath" />,还是先同步到
|
||||||
|
/// <see cref="RuntimeCacheRootPath" /> 再交给底层 <see cref="YamlConfigLoader" />。
|
||||||
|
/// 只有源根目录可直接作为普通文件系统目录访问时,<see cref="CanEnableHotReload" /> 才会返回
|
||||||
|
/// <see langword="true" />。
|
||||||
|
/// </remarks>
|
||||||
public GodotYamlConfigLoader(GodotYamlConfigLoaderOptions options)
|
public GodotYamlConfigLoader(GodotYamlConfigLoaderOptions options)
|
||||||
: this(options, GodotYamlConfigEnvironment.Default)
|
: this(options, GodotYamlConfigEnvironment.Default)
|
||||||
{
|
{
|
||||||
@ -115,6 +129,19 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
/// <param name="registry">要被热重载更新的配置注册表。</param>
|
/// <param name="registry">要被热重载更新的配置注册表。</param>
|
||||||
/// <param name="options">热重载选项;为空时使用默认值。</param>
|
/// <param name="options">热重载选项;为空时使用默认值。</param>
|
||||||
/// <returns>用于停止监听的注销句柄。</returns>
|
/// <returns>用于停止监听的注销句柄。</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// 当当前实例必须通过运行时缓存访问配置源,无法直接监听真实源目录时抛出。
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">
|
||||||
|
/// 当 <paramref name="options" /> 的防抖延迟小于 <see cref="TimeSpan.Zero" /> 时,
|
||||||
|
/// 底层 <see cref="YamlConfigLoader" /> 会拒绝启用热重载。
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// 调用前应先检查 <see cref="CanEnableHotReload" />。
|
||||||
|
/// 当 <see cref="SourceRootPath" /> 只能通过缓存同步访问时,拒绝启用热重载是为了避免监听缓存副本后误导调用方,
|
||||||
|
/// 让其误以为源目录改动会被自动反映到运行时。
|
||||||
|
/// </remarks>
|
||||||
public IUnRegister EnableHotReload(
|
public IUnRegister EnableHotReload(
|
||||||
IConfigRegistry registry,
|
IConfigRegistry registry,
|
||||||
YamlConfigHotReloadOptions? options = null)
|
YamlConfigHotReloadOptions? options = null)
|
||||||
@ -171,7 +198,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
var sourceDirectoryPath = CombinePath(SourceRootPath, representative.ConfigRelativePath);
|
var sourceDirectoryPath = CombinePath(SourceRootPath, representative.ConfigRelativePath);
|
||||||
var targetDirectoryPath = CombineAbsolutePath(LoaderRootPath, representative.ConfigRelativePath);
|
var targetDirectoryPath = CombineAbsolutePath(LoaderRootPath, representative.ConfigRelativePath);
|
||||||
|
|
||||||
ResetDirectory(targetDirectoryPath);
|
ResetDirectory(representative.TableName, sourceDirectoryPath, targetDirectoryPath);
|
||||||
CopyYamlFilesInDirectory(
|
CopyYamlFilesInDirectory(
|
||||||
representative.TableName,
|
representative.TableName,
|
||||||
sourceDirectoryPath,
|
sourceDirectoryPath,
|
||||||
@ -215,8 +242,6 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
configDirectoryPath: DescribePath(sourceDirectoryPath));
|
configDirectoryPath: DescribePath(sourceDirectoryPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(targetDirectoryPath);
|
|
||||||
|
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@ -303,14 +328,28 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetDirectory(string directoryPath)
|
private void ResetDirectory(string tableName, string sourceDirectoryPath, string targetDirectoryPath)
|
||||||
{
|
{
|
||||||
if (Directory.Exists(directoryPath))
|
try
|
||||||
{
|
{
|
||||||
Directory.Delete(directoryPath, recursive: true);
|
if (Directory.Exists(targetDirectoryPath))
|
||||||
}
|
{
|
||||||
|
Directory.Delete(targetDirectoryPath, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(directoryPath);
|
Directory.CreateDirectory(targetDirectoryPath);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
var describedSourceDirectoryPath = DescribePath(sourceDirectoryPath);
|
||||||
|
throw CreateConfigLoadException(
|
||||||
|
ConfigLoadFailureKind.ConfigFileReadFailed,
|
||||||
|
tableName,
|
||||||
|
$"Failed to reset runtime cache directory '{targetDirectoryPath}' while preparing config directory '{describedSourceDirectoryPath}'.",
|
||||||
|
configDirectoryPath: describedSourceDirectoryPath,
|
||||||
|
detail: $"Runtime cache directory: {targetDirectoryPath}.",
|
||||||
|
innerException: exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string EnsureAbsolutePath(string path, string optionName)
|
private string EnsureAbsolutePath(string path, string optionName)
|
||||||
@ -417,6 +456,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
string? configDirectoryPath = null,
|
string? configDirectoryPath = null,
|
||||||
string? yamlPath = null,
|
string? yamlPath = null,
|
||||||
string? schemaPath = null,
|
string? schemaPath = null,
|
||||||
|
string? detail = null,
|
||||||
Exception? innerException = null)
|
Exception? innerException = null)
|
||||||
{
|
{
|
||||||
return new ConfigLoadException(
|
return new ConfigLoadException(
|
||||||
@ -425,7 +465,8 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
tableName,
|
tableName,
|
||||||
configDirectoryPath: configDirectoryPath,
|
configDirectoryPath: configDirectoryPath,
|
||||||
yamlPath: yamlPath,
|
yamlPath: yamlPath,
|
||||||
schemaPath: schemaPath),
|
schemaPath: schemaPath,
|
||||||
|
detail: detail),
|
||||||
message,
|
message,
|
||||||
innerException);
|
innerException);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user