diff --git a/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs b/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs index 6aa7a8e2..bef0737a 100644 --- a/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs +++ b/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs @@ -6,9 +6,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; using GFramework.Game.Abstractions.Config; -using GFramework.Game.Config; using GFramework.Godot.Config; -using NUnit.Framework; namespace GFramework.Godot.Tests.Config; @@ -18,6 +16,10 @@ namespace GFramework.Godot.Tests.Config; [TestFixture] public sealed class GodotYamlConfigLoaderTests { + private string _resourceRoot = null!; + private string _testRoot = null!; + private string _userRoot = null!; + /// /// 为每个测试准备独立的资源根目录与用户目录。 /// @@ -46,10 +48,6 @@ public sealed class GodotYamlConfigLoaderTests } } - private string _resourceRoot = null!; - private string _testRoot = null!; - private string _userRoot = null!; - /// /// 验证导出态会把注册过的 YAML 与 schema 文本同步到运行时缓存,再交给底层加载器。 /// @@ -205,10 +203,12 @@ public sealed class GodotYamlConfigLoaderTests /// /// 验证加载器自身会拒绝可能逃逸缓存根目录的非法配置目录路径,即使调用方绕过了公开构造约束。 /// - [Test] - public void LoadAsync_Should_Reject_Invalid_Config_Relative_Path_When_Metadata_Is_Corrupted() + [TestCase("../outside")] + [TestCase("schemas:bad/monster")] + public void LoadAsync_Should_Reject_Invalid_Config_Relative_Path_When_Metadata_Is_Corrupted( + string configRelativePath) { - var corruptedSource = CreateUnsafeTableSource("monster", "../outside"); + var corruptedSource = CreateUnsafeTableSource("monster", configRelativePath); var loader = CreateLoader( isEditor: false, tableSources: [corruptedSource], @@ -223,8 +223,10 @@ public sealed class GodotYamlConfigLoaderTests /// /// 验证加载器自身会拒绝可能逃逸缓存根目录的非法 schema 路径,即使调用方绕过了公开构造约束。 /// - [Test] - public void LoadAsync_Should_Reject_Invalid_Schema_Relative_Path_When_Metadata_Is_Corrupted() + [TestCase("../schemas/monster.schema.json")] + [TestCase("schemas:bad/monster.schema.json")] + public void LoadAsync_Should_Reject_Invalid_Schema_Relative_Path_When_Metadata_Is_Corrupted( + string schemaRelativePath) { WriteFile( _resourceRoot, @@ -235,7 +237,7 @@ public sealed class GodotYamlConfigLoaderTests hp: 10 """); - var corruptedSource = CreateUnsafeTableSource("monster", "monster", "../schemas/monster.schema.json"); + var corruptedSource = CreateUnsafeTableSource("monster", "monster", schemaRelativePath); var loader = CreateLoader( isEditor: false, tableSources: [corruptedSource], diff --git a/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs b/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs index 5e4998ce..ccb8ab6a 100644 --- a/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs +++ b/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs @@ -1,6 +1,5 @@ using System; using GFramework.Godot.Config; -using NUnit.Framework; namespace GFramework.Godot.Tests.Config; @@ -27,6 +26,8 @@ public sealed class GodotYamlConfigTableSourceTests [TestCase(@"C:\monster")] [TestCase("res://monster")] [TestCase("user://monster")] + [TestCase("schemas:bad/monster")] + [TestCase(@"schemas:bad\monster")] public void Constructor_Should_Throw_When_Config_Relative_Path_Is_Not_Safe(string configRelativePath) { var exception = Assert.Throws(() => @@ -52,6 +53,8 @@ public sealed class GodotYamlConfigTableSourceTests [TestCase(@"C:\schemas\monster.schema.json")] [TestCase("res://schemas/monster.schema.json")] [TestCase("user://schemas/monster.schema.json")] + [TestCase("schemas:bad/monster.schema.json")] + [TestCase(@"schemas:bad\monster.schema.json")] public void Constructor_Should_Throw_When_Schema_Relative_Path_Is_Not_Safe(string schemaRelativePath) { var exception = Assert.Throws(() => diff --git a/GFramework.Godot/Config/GodotYamlConfigLoader.cs b/GFramework.Godot/Config/GodotYamlConfigLoader.cs index e4cf2c09..0e32867a 100644 --- a/GFramework.Godot/Config/GodotYamlConfigLoader.cs +++ b/GFramework.Godot/Config/GodotYamlConfigLoader.cs @@ -3,7 +3,6 @@ using GFramework.Core.Abstractions.Events; using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; using GFramework.Godot.Extensions; -using FileAccess = Godot.FileAccess; namespace GFramework.Godot.Config; @@ -440,6 +439,14 @@ public sealed class GodotYamlConfigLoader : IConfigLoader throw new ArgumentException("Relative path must be an unrooted path.", nameof(relativePath)); } + // Reject ':' in later segments as well so Windows-invalid names and ADS-like syntax never reach file APIs. + if (normalizedPath.Contains(':', StringComparison.Ordinal)) + { + throw new ArgumentException( + "Relative path must not contain ':' characters.", + nameof(relativePath)); + } + var segments = normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries); if (segments.Any(static segment => segment is "." or "..")) { diff --git a/GFramework.Godot/Config/GodotYamlConfigTableSource.cs b/GFramework.Godot/Config/GodotYamlConfigTableSource.cs index 4d28826a..c851751d 100644 --- a/GFramework.Godot/Config/GodotYamlConfigTableSource.cs +++ b/GFramework.Godot/Config/GodotYamlConfigTableSource.cs @@ -13,11 +13,12 @@ public sealed class GodotYamlConfigTableSource /// 运行时表名称。 /// /// 相对配置根目录的 YAML 目录。 - /// 该路径必须保持为无根相对路径,且不能包含 ...res://user:// 或磁盘根路径前缀。 + /// 该路径必须保持为无根相对路径,且不能包含 ...res://user://: + /// 或磁盘根路径前缀。 /// /// /// 相对配置根目录的 schema 文件路径;未启用 schema 时为空。 - /// 如果提供,同样必须保持为无根相对路径,且不能包含 ... 或任何绝对路径前缀。 + /// 如果提供,同样必须保持为无根相对路径,且不能包含 ...: 或任何绝对路径前缀。 /// /// /// @@ -72,13 +73,13 @@ public sealed class GodotYamlConfigTableSource /// /// 获取相对配置根目录的 YAML 目录路径。 - /// 该值始终保持为无根相对路径,不会包含 ... 段。 + /// 该值始终保持为无根相对路径,不会包含 ...: 段。 /// public string ConfigRelativePath { get; } /// /// 获取相对配置根目录的 schema 文件路径;未启用 schema 校验时为空。 - /// 该值在非空时始终保持为无根相对路径,不会包含 ... 段。 + /// 该值在非空时始终保持为无根相对路径,不会包含 ...: 段。 /// public string? SchemaRelativePath { get; } @@ -94,6 +95,11 @@ public sealed class GodotYamlConfigTableSource return false; } + if (normalizedPath.Contains(':', StringComparison.Ordinal)) + { + return false; + } + foreach (var segment in normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries)) { if (segment is "." or "..")