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 "..")