From f6688ce573194ca58c91aff25f76b1199a91c6ee Mon Sep 17 00:00:00 2001 From: cneicy Date: Fri, 28 Aug 2026 03:44:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E4=BB=85=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E6=8E=88=E6=9D=83=E7=9A=84=E5=A4=96=E9=83=A8?= =?UTF-8?q?=20DLL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 17 +++ Editor/ShrinkModFrameworkSettingsMenu.cs | 49 ++++++++ Editor/ShrinkModFrameworkSettingsMenu.cs.meta | 11 ++ README.md | 52 +++++---- Runtime/Bootstrap/ShrinkModBootstrap.cs | 2 +- .../Bootstrap/ShrinkModFrameworkSettings.cs | 19 ++- .../Bootstrap/ShrinkModRuntimeBootstrap.cs | 13 ++- .../Context/ShrinkModComponentDiscovery.cs | 9 +- Runtime/Context/ShrinkModCordisRuntime.cs | 8 +- .../ShrinkExternalModAssemblyLoader.cs | 109 ++++++++++++++---- Runtime/Loading/ShrinkModLoader.cs | 56 +++++++-- Tests/ShrinkModContextHostTests.cs | 79 ++++++++++++- package.json | 2 +- 13 files changed, 352 insertions(+), 74 deletions(-) create mode 100644 Editor/ShrinkModFrameworkSettingsMenu.cs create mode 100644 Editor/ShrinkModFrameworkSettingsMenu.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d7211..1b0cf71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ 本文件记录 `ShrinkModFramework` 在当前工作区中的包内变更。 +## [0.2.3] - 2026-08-28 + +### Added + +- 新增 `ShrinkModLoader.LoadAuthorized(settings, authorizedDllPaths)`,只从调用方提交的精确 DLL 白名单加载外部模组。 +- 恢复公共入口 `ShrinkModRuntimeBootstrap.InitializeDriver(...)`,供安全启动链显式初始化运行时驱动。 + +### Changed + +- ContextHost 与旧加载路径不再递归扫描外部模组目录;revision 刷新只复用已授权路径。 +- `autoLoadOnStartup`、`enableExternalDllMods`、`watchExternalModsDirectory` 默认关闭。 +- `ShrinkModFrameworkSettings` 优先从标准 ShrinkSDK 资源目录加载,创建菜单同步写入该目录。 + +### Fixed + +- 授权 DLL 替换失败时继续恢复此前已提交的 SHA-256 revision 与模组组合。 + ## [0.2.1] - 2026-08-26 - 将 EventBus 生成器源码和 fixture builder 收敛到各自 UPM 包的 `Tools~`,并为独立包导出提供随包分析器回退。 diff --git a/Editor/ShrinkModFrameworkSettingsMenu.cs b/Editor/ShrinkModFrameworkSettingsMenu.cs new file mode 100644 index 0000000..d0953cd --- /dev/null +++ b/Editor/ShrinkModFrameworkSettingsMenu.cs @@ -0,0 +1,49 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +namespace ShrinkModFramework.Editor +{ + public static class ShrinkModFrameworkSettingsMenu + { + private const string SettingsDirectory = + "Assets/Resources/GameAssets/Runtime/Data/ShrinkSDK"; + private const string SettingsAssetPath = + SettingsDirectory + "/ShrinkModFrameworkSettings.asset"; + + [MenuItem("ShrinkSDK/模组/创建设置")] + public static void CreateSettingsAsset() + { + EnsureFolder(SettingsDirectory); + + var existing = AssetDatabase.LoadAssetAtPath(SettingsAssetPath); + if (existing != null) + { + Selection.activeObject = existing; + EditorGUIUtility.PingObject(existing); + return; + } + + var asset = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(asset, SettingsAssetPath); + AssetDatabase.SaveAssets(); + Selection.activeObject = asset; + EditorGUIUtility.PingObject(asset); + Debug.Log("[ShrinkModFramework] 已创建 ShrinkModFrameworkSettings.asset"); + } + + private static void EnsureFolder(string path) + { + var segments = path.Split('/'); + var current = segments[0]; + for (var i = 1; i < segments.Length; i++) + { + var next = current + "/" + segments[i]; + if (!AssetDatabase.IsValidFolder(next)) + AssetDatabase.CreateFolder(current, segments[i]); + current = next; + } + } + } +} +#endif diff --git a/Editor/ShrinkModFrameworkSettingsMenu.cs.meta b/Editor/ShrinkModFrameworkSettingsMenu.cs.meta new file mode 100644 index 0000000..2e659a0 --- /dev/null +++ b/Editor/ShrinkModFrameworkSettingsMenu.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 400ef5e3613144f9b9f248d259b84a22 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 5ad67df..d6a26d0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - 依赖解析 - 生命周期阶段 - 内容注册表 -- 自动启动 +- 可选显式启动 - 外部 DLL 模组热加载 - Harmony 热补丁接入 - 网络同步通道 @@ -31,17 +31,19 @@ - 内置的资源包系统、命令系统、配方编辑器 - 内置的具体联网实现 -## 自动启动 +## 启动 -现在默认**不需要**把 `ShrinkModBootstrap` 挂到场景里。 - -框架会通过: +安全默认值不会自动启动或扫描外部 DLL。需要装载工程内模组时,可以显式调用 +`ShrinkModLoader.LoadAll(settings)`;需要外部代码模组时,调用方必须先完成自己的清单、 +启用状态和哈希校验,再提交精确白名单: ```csharp -[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] +ShrinkModRuntimeBootstrap.InitializeDriver(settings); +ShrinkModLoader.LoadAuthorized(settings, authorizedDllPaths); ``` -自动读取 `ShrinkModFrameworkSettings` 并调用装载流程。 +只有显式打开 `autoLoadOnStartup` 时,`AfterAssembliesLoaded` 入口才会装载工程内模组; +该自动入口仍不加载任何未授权外部 DLL。 相关入口: @@ -52,12 +54,14 @@ ## 配置文件 -创建 `ShrinkModFrameworkSettings` 资产后,框架会自动查找它。 +菜单 `ShrinkSDK/模组/创建设置` 会在 +`Assets/Resources/GameAssets/Runtime/Data/ShrinkSDK/ShrinkModFrameworkSettings.asset` +创建配置。运行时优先加载该路径,并保留旧根路径回退。 关键配置包括: - `autoLoadOnStartup` - - 是否在启动时自动装载模组 + - 是否在启动时自动装载工程内模组,默认关闭 - `useContextHost` - 默认开启:把模组四阶段放进 `ShrinkModContextHost`,替换失败恢复旧组件源;关闭后回退旧 Loader - `verboseLogging` @@ -65,11 +69,11 @@ - `assemblyNamePrefixes` - 只扫描指定前缀的程序集 - `enableExternalDllMods` - - 是否启用外部 DLL 模组 + - 是否允许显式白名单中的外部 DLL 模组,默认关闭 - `externalModsFolderName` - 外部模组目录名,默认 `Mods` - `watchExternalModsDirectory` - - 是否自动监听目录变化并协调外部 DLL revision 变化 + - 是否监听文件变化并重新提交既有白名单,默认关闭;监听不会扩大授权范围 - `externalModsReloadDelaySeconds` - 文件变更后延迟多少秒再尝试热加载 - `externalAssemblyRevisionSoftLimit` @@ -182,13 +186,8 @@ public class DemoSafeMod : ShrinkModBase ## 外部 DLL 模组热加载 -框架会扫描: - -`Application.persistentDataPath/` - -默认就是: - -`Application.persistentDataPath/Mods` +框架不会递归扫描模组目录。调用方必须把每个允许进入 AppDomain 的入口 DLL 绝对路径 +作为白名单提交给 `LoadAuthorized`;目录中未列出的 DLL 不会被读取或加载。 ContextLoader 装载规则: @@ -199,16 +198,19 @@ ContextLoader 装载规则: - 不支持 IL2CPP Player 动态程序集加载 - 支持同目录依赖程序集解析 -你可以在运行时调用: +首次安全加载: ```csharp -ShrinkModLoader.LoadNewExternalMods(); +ShrinkModRuntimeBootstrap.InitializeDriver(settings); +ShrinkModLoader.LoadAuthorized(settings, authorizedDllPaths); ``` -默认会扫描当前 DLL revision,并把新增、替换、删除映射为一个完整期望组合; -变更事务失败时保留旧模组组合。设置 `useContextHost = false` 才回退为仅新增 DLL 的旧路径。 +后续可调用 `ShrinkModLoader.LoadNewExternalMods(settings)` 重新读取同一白名单的 revision, +把新增、替换、删除映射为完整期望组合;变更事务失败时保留旧模组组合。设置 +`useContextHost = false` 时,旧路径同样只读取显式白名单,但仍保持只增不减的兼容语义。 -如果 `watchExternalModsDirectory = true`,框架还会监听新增、修改、删除、重命名事件,经过主线程 debouncer 后提交一次完整组合;同一 burst 内的中间坏文件不会覆盖当前有效 revision。 +如果 `watchExternalModsDirectory = true`,框架会监听目录变化并在主线程 debouncer 后重新提交 +既有白名单;未授权文件即使触发通知也不会被加载。同一 burst 内的中间坏文件不会覆盖当前有效 revision。 ### 常驻 revision 诊断 @@ -697,8 +699,8 @@ public sealed partial class DemoFullMod : ShrinkModBase 现在这套框架已经从“只能在工程内静态发现模组”的骨架,升级成了: -- 自动启动 -- 可增量接入外部 DLL +- 可显式托管启动 +- 可按精确白名单增量接入外部 DLL - 可选 Harmony 补丁 - 可扩展的网络同步框架 diff --git a/Runtime/Bootstrap/ShrinkModBootstrap.cs b/Runtime/Bootstrap/ShrinkModBootstrap.cs index bf0c89c..81a149d 100644 --- a/Runtime/Bootstrap/ShrinkModBootstrap.cs +++ b/Runtime/Bootstrap/ShrinkModBootstrap.cs @@ -9,7 +9,7 @@ namespace ShrinkModFramework [SerializeField] private ShrinkModFrameworkSettings settingsOverride; [Header("Bootstrap")] - [SerializeField] private bool autoLoadOnAwake = true; + [SerializeField] private bool autoLoadOnAwake; private static bool _bootstrapped; diff --git a/Runtime/Bootstrap/ShrinkModFrameworkSettings.cs b/Runtime/Bootstrap/ShrinkModFrameworkSettings.cs index cdafc81..53aab77 100644 --- a/Runtime/Bootstrap/ShrinkModFrameworkSettings.cs +++ b/Runtime/Bootstrap/ShrinkModFrameworkSettings.cs @@ -5,6 +5,10 @@ namespace ShrinkModFramework [CreateAssetMenu(fileName = "ShrinkModFrameworkSettings", menuName = "ShrinkSDK/模组/模组设置")] public class ShrinkModFrameworkSettings : ScriptableObject { + public const string PreferredResourcesPath = + "GameAssets/Runtime/Data/ShrinkSDK/ShrinkModFrameworkSettings"; + public const string LegacyResourcesPath = "ShrinkModFrameworkSettings"; + private static ShrinkModFrameworkSettings _instance; public static ShrinkModFrameworkSettings Instance @@ -13,7 +17,9 @@ namespace ShrinkModFramework { if (_instance) return _instance; - _instance = Resources.Load("ShrinkModFrameworkSettings"); + _instance = Resources.Load(PreferredResourcesPath); + if (!_instance) + _instance = Resources.Load(LegacyResourcesPath); #if UNITY_EDITOR if (!_instance) @@ -39,8 +45,13 @@ namespace ShrinkModFramework internal set => _instance = value; } + internal static void ResetCachedInstance() + { + _instance = null; + } + [Header("Bootstrap")] - public bool autoLoadOnStartup = true; + public bool autoLoadOnStartup; [Tooltip("使用 ShrinkContextHost 协调模组组件;关闭时回退到旧的只增不减 ShrinkModLoader。")] public bool useContextHost = true; @@ -51,10 +62,10 @@ namespace ShrinkModFramework public string[] assemblyNamePrefixes = new string[0]; [Header("External Mods")] - public bool enableExternalDllMods = true; + public bool enableExternalDllMods; public bool autoCreateExternalModsDirectory = true; public string externalModsFolderName = "Mods"; - public bool watchExternalModsDirectory = true; + public bool watchExternalModsDirectory; public float externalModsReloadDelaySeconds = 0.5f; [Min(1)] [Tooltip("Mono 下外部程序集 revision 会常驻;历史数量达到该软阈值后提示 Domain Reload/重启。")] diff --git a/Runtime/Bootstrap/ShrinkModRuntimeBootstrap.cs b/Runtime/Bootstrap/ShrinkModRuntimeBootstrap.cs index 15230b8..2991d50 100644 --- a/Runtime/Bootstrap/ShrinkModRuntimeBootstrap.cs +++ b/Runtime/Bootstrap/ShrinkModRuntimeBootstrap.cs @@ -4,6 +4,15 @@ namespace ShrinkModFramework { public static class ShrinkModRuntimeBootstrap { + /// + /// 显式创建或更新模组运行时驱动。安全启动链可先调用此入口,再调用 + /// ShrinkModLoader.LoadAuthorized 提交已校验的 DLL 白名单。 + /// + public static void InitializeDriver(ShrinkModFrameworkSettings settings = null) + { + ShrinkModRuntimeDriver.EnsureCreated(settings ?? ShrinkModFrameworkSettings.Instance); + } + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetStaticStateForPlayMode() { @@ -15,11 +24,11 @@ namespace ShrinkModFramework private static void AutoLoad() { var settings = ShrinkModFrameworkSettings.Instance; - ShrinkModRuntimeDriver.EnsureCreated(settings); - if (settings && !settings.autoLoadOnStartup) return; + InitializeDriver(settings); + try { ShrinkModLoader.LoadAll(settings); diff --git a/Runtime/Context/ShrinkModComponentDiscovery.cs b/Runtime/Context/ShrinkModComponentDiscovery.cs index 50d0619..5edc0e2 100644 --- a/Runtime/Context/ShrinkModComponentDiscovery.cs +++ b/Runtime/Context/ShrinkModComponentDiscovery.cs @@ -12,10 +12,13 @@ namespace ShrinkModFramework /// internal static class ShrinkModComponentDiscovery { - public static IReadOnlyList Discover(ShrinkModFrameworkSettings settings, - bool verboseLogging) + public static IReadOnlyList Discover( + ShrinkModFrameworkSettings settings, + bool verboseLogging, + IEnumerable authorizedDllPaths) { - ShrinkExternalModAssemblyLoader.ScanExternalAssemblyRevisions(settings, verboseLogging); + ShrinkExternalModAssemblyLoader.ScanExternalAssemblyRevisions( + settings, verboseLogging, authorizedDllPaths); var results = new List(); var prefixes = settings?.assemblyNamePrefixes; diff --git a/Runtime/Context/ShrinkModCordisRuntime.cs b/Runtime/Context/ShrinkModCordisRuntime.cs index fd5594f..0b218f5 100644 --- a/Runtime/Context/ShrinkModCordisRuntime.cs +++ b/Runtime/Context/ShrinkModCordisRuntime.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Cysharp.Threading.Tasks; namespace ShrinkModFramework @@ -19,7 +20,8 @@ namespace ShrinkModFramework new Dictionary(); public static async UniTask> ApplyDiscoveredAsync( - ShrinkModFrameworkSettings settings = null) + ShrinkModFrameworkSettings settings = null, + IEnumerable authorizedDllPaths = null) { settings ??= ShrinkModFrameworkSettings.Instance; var verboseLogging = settings == null || settings.verboseLogging; @@ -34,7 +36,9 @@ namespace ShrinkModFramework ShrinkModNetworkManager.Configure(settings == null || settings.enableNetworkSync, verboseLogging); try { - var sources = ShrinkModComponentDiscovery.Discover(settings, verboseLogging); + var authorizedPaths = authorizedDllPaths?.ToArray() ?? Array.Empty(); + var sources = ShrinkModComponentDiscovery.Discover( + settings, verboseLogging, authorizedPaths); await _host.ApplyAsync(sources); } catch diff --git a/Runtime/Loading/ShrinkExternalModAssemblyLoader.cs b/Runtime/Loading/ShrinkExternalModAssemblyLoader.cs index efc1597..4f9aab7 100644 --- a/Runtime/Loading/ShrinkExternalModAssemblyLoader.cs +++ b/Runtime/Loading/ShrinkExternalModAssemblyLoader.cs @@ -34,17 +34,22 @@ namespace ShrinkModFramework private static bool _resolveRegistered; private static int _lastWarnedResidentCount; - public static IReadOnlyList LoadExternalAssemblies(ShrinkModFrameworkSettings settings, bool verboseLogging) + public static IReadOnlyList LoadExternalAssemblies( + ShrinkModFrameworkSettings settings, + bool verboseLogging, + IEnumerable authorizedDllPaths = null) { - return ScanExternalAssemblyRevisions(settings, verboseLogging) + return ScanExternalAssemblyRevisions(settings, verboseLogging, authorizedDllPaths) .Select(revision => revision.Assembly) .ToArray(); } internal static IReadOnlyList ScanExternalAssemblyRevisions( - ShrinkModFrameworkSettings settings, bool verboseLogging) + ShrinkModFrameworkSettings settings, + bool verboseLogging, + IEnumerable authorizedDllPaths = null) { - if (settings != null && !settings.enableExternalDllMods) + if (settings == null || !settings.enableExternalDllMods) { CurrentAssemblyRevisions.Clear(); KnownAssemblyFiles.Clear(); @@ -52,27 +57,23 @@ namespace ShrinkModFramework } #if ENABLE_IL2CPP && !UNITY_EDITOR - Debug.LogWarning("[ShrinkModFramework] IL2CPP 运行时不支持外部 DLL 热加载,已跳过外部模组扫描。"); + Debug.LogWarning("[ShrinkModFramework] IL2CPP 运行时不支持外部 DLL 加载,已跳过授权模组。"); return Array.Empty(); #else - var modsDirectory = GetExternalModsDirectory(settings); - if (settings == null || settings.autoCreateExternalModsDirectory) - Directory.CreateDirectory(modsDirectory); - RegisterAssemblyResolve(); - var dllPaths = Directory.GetFiles(modsDirectory, "*.dll", SearchOption.AllDirectories) - .Select(Path.GetFullPath) - .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) - .ToArray(); - var presentPaths = new HashSet(dllPaths, StringComparer.OrdinalIgnoreCase); + var dllPaths = NormalizeAuthorizedPaths(authorizedDllPaths); + var presentPaths = new HashSet( + dllPaths.Where(File.Exists), + StringComparer.OrdinalIgnoreCase); - foreach (var dllPath in dllPaths + KnownAssemblyFiles.Clear(); + foreach (var dllPath in presentPaths) + KnownAssemblyFiles[Path.GetFileNameWithoutExtension(dllPath)] = dllPath; + + foreach (var dllPath in presentPaths .OrderBy(path => path, StringComparer.OrdinalIgnoreCase)) { - var assemblyName = Path.GetFileNameWithoutExtension(dllPath); - KnownAssemblyFiles[assemblyName] = dllPath; - try { var bytes = File.ReadAllBytes(dllPath); @@ -131,6 +132,41 @@ namespace ShrinkModFramework #endif } + private static string[] NormalizeAuthorizedPaths(IEnumerable authorizedDllPaths) + { + if (authorizedDllPaths == null) + return Array.Empty(); + + var normalized = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var rawPath in authorizedDllPaths) + { + if (string.IsNullOrWhiteSpace(rawPath)) + continue; + + string fullPath; + try + { + fullPath = Path.GetFullPath(rawPath.Trim()); + } + catch (Exception exception) + { + throw new ArgumentException($"授权 DLL 路径无效:{rawPath}", + nameof(authorizedDllPaths), exception); + } + + if (!string.Equals(Path.GetExtension(fullPath), ".dll", + StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"授权路径不是 DLL:{fullPath}", + nameof(authorizedDllPaths)); + } + + normalized.Add(fullPath); + } + + return normalized.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToArray(); + } + internal static bool IsExternalAssembly(Assembly assembly) => assembly != null && ExternalAssemblyHistory.ContainsKey(assembly); @@ -268,13 +304,38 @@ namespace ShrinkModFramework if (!KnownAssemblyFiles.TryGetValue(requestedName, out var path) || !File.Exists(path)) return null; - try + lock (ResolveLock) { - return Assembly.Load(File.ReadAllBytes(path)); - } - catch - { - return null; + try + { + var bytes = File.ReadAllBytes(path); + var revision = ComputeSha256(bytes); + if (CurrentAssemblyRevisions.TryGetValue(path, out var current) && + string.Equals(current.Revision, revision, StringComparison.Ordinal)) + { + return current.Assembly; + } + + var pdbPath = Path.ChangeExtension(path, ".pdb"); + var pdbBytes = File.Exists(pdbPath) ? File.ReadAllBytes(pdbPath) : null; + var assembly = pdbBytes != null + ? Assembly.Load(bytes, pdbBytes) + : Assembly.Load(bytes); + var loadedRevision = new ExternalAssemblyRevision + { + Path = path, + Revision = revision, + Assembly = assembly, + LoadedBytes = bytes.LongLength + (pdbBytes?.LongLength ?? 0L) + }; + CurrentAssemblyRevisions[path] = loadedRevision; + ExternalAssemblyHistory[assembly] = loadedRevision; + return assembly; + } + catch + { + return null; + } } } } diff --git a/Runtime/Loading/ShrinkModLoader.cs b/Runtime/Loading/ShrinkModLoader.cs index 5247ae8..fddbe8d 100644 --- a/Runtime/Loading/ShrinkModLoader.cs +++ b/Runtime/Loading/ShrinkModLoader.cs @@ -18,6 +18,7 @@ namespace ShrinkModFramework private static readonly Dictionary LoadedMods = new(StringComparer.Ordinal); private static readonly List LoadSequence = new(); private static readonly ShrinkModRegistryManager RegistryManager = new(); + private static string[] _authorizedDllPaths = Array.Empty(); public static bool IsLoaded { get; private set; } public static IReadOnlyDictionary Mods => @@ -27,8 +28,35 @@ namespace ShrinkModFramework public static event Action> OnAllModsReady; public static IReadOnlyDictionary LoadAll(ShrinkModFrameworkSettings settings = null) + => LoadInternal(settings, Array.Empty()); + + /// + /// 加载工程内模组,并且只加载调用方显式授权的外部 DLL 路径。 + /// 路径集合是完整白名单,不会递归扫描模组目录;后续 revision 刷新也只复用该白名单。 + /// + public static IReadOnlyDictionary LoadAuthorized( + ShrinkModFrameworkSettings settings, + IEnumerable authorizedDllPaths) + { + if (authorizedDllPaths == null) + throw new ArgumentNullException(nameof(authorizedDllPaths)); + + return LoadInternal(settings, authorizedDllPaths); + } + + private static IReadOnlyDictionary LoadInternal( + ShrinkModFrameworkSettings settings, + IEnumerable authorizedDllPaths) { settings ??= ShrinkModFrameworkSettings.Instance; + _authorizedDllPaths = authorizedDllPaths + .Where(path => !string.IsNullOrWhiteSpace(path)) + .Select(path => path.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (settings == null || settings.useContextHost) + return ApplyContextComposition(settings, _authorizedDllPaths); if (IsLoaded) { @@ -36,15 +64,13 @@ namespace ShrinkModFramework return Mods; } - if (settings == null || settings.useContextHost) - return ApplyContextComposition(settings); - var verboseLogging = settings == null || settings.verboseLogging; try { ShrinkModNetworkManager.Configure(settings == null || settings.enableNetworkSync, verboseLogging); - ShrinkExternalModAssemblyLoader.LoadExternalAssemblies(settings, verboseLogging); + ShrinkExternalModAssemblyLoader.LoadExternalAssemblies( + settings, verboseLogging, _authorizedDllPaths); var discovered = DiscoverMods(settings); var ordered = ResolveLoadOrder(discovered, allowExistingLoadedDependencies: true); @@ -73,12 +99,13 @@ namespace ShrinkModFramework { settings ??= ShrinkModFrameworkSettings.Instance; if (settings == null || settings.useContextHost) - return ApplyContextComposition(settings); + return ApplyContextComposition(settings, _authorizedDllPaths); if (!IsLoaded) - return LoadAll(settings); + return LoadInternal(settings, _authorizedDllPaths); var verboseLogging = settings == null || settings.verboseLogging; - ShrinkExternalModAssemblyLoader.LoadExternalAssemblies(settings, verboseLogging); + ShrinkExternalModAssemblyLoader.LoadExternalAssemblies( + settings, verboseLogging, _authorizedDllPaths); var discovered = DiscoverMods(settings) .Where(mod => !LoadedMods.ContainsKey(mod.Info.ModId)) @@ -145,6 +172,7 @@ namespace ShrinkModFramework ShrinkModNetworkManager.ResetForDomainReload(); ShrinkExternalModAssemblyLoader.ResetForTesting(); ShrinkHarmonyPatchService.ResetForTesting(); + _authorizedDllPaths = Array.Empty(); } internal static void ResetForDomainReload() @@ -160,17 +188,22 @@ namespace ShrinkModFramework ShrinkModNetworkManager.ResetForDomainReload(); ShrinkExternalModAssemblyLoader.ResetForDomainReload(); ShrinkHarmonyPatchService.ResetForTesting(); + _authorizedDllPaths = Array.Empty(); } private static IReadOnlyDictionary ApplyContextComposition( - ShrinkModFrameworkSettings settings) + ShrinkModFrameworkSettings settings, + IEnumerable authorizedDllPaths) { var previousGenerations = Mods.ToDictionary( pair => pair.Key, pair => pair.Value.Generation, StringComparer.Ordinal); - var result = ShrinkModCordisRuntime.ApplyDiscoveredAsync(settings).GetAwaiter().GetResult(); + var result = ShrinkModCordisRuntime + .ApplyDiscoveredAsync(settings, authorizedDllPaths) + .GetAwaiter() + .GetResult(); IsLoaded = true; foreach (var pair in result.OrderBy(pair => pair.Key, StringComparer.Ordinal)) { @@ -206,6 +239,11 @@ namespace ShrinkModFramework foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { + if (ShrinkExternalModAssemblyLoader.IsExternalAssembly(assembly) && + !ShrinkExternalModAssemblyLoader.TryGetCurrentRevision(assembly, out _)) + { + continue; + } if (!ShouldScanAssembly(assembly, prefixes)) continue; diff --git a/Tests/ShrinkModContextHostTests.cs b/Tests/ShrinkModContextHostTests.cs index 0f1f91c..e121d6d 100644 --- a/Tests/ShrinkModContextHostTests.cs +++ b/Tests/ShrinkModContextHostTests.cs @@ -286,7 +286,23 @@ namespace ShrinkModFramework.Tests } [Test] - public void ExternalDllRevision_FailedReplacementRestoresPreviousAndBrokenBytesDoNotReplaceIt() + public void Settings_Defaults_DoNotAutoLoadOrWatchExternalDlls() + { + var settings = ScriptableObject.CreateInstance(); + try + { + Assert.IsFalse(settings.autoLoadOnStartup); + Assert.IsFalse(settings.enableExternalDllMods); + Assert.IsFalse(settings.watchExternalModsDirectory); + } + finally + { + Object.DestroyImmediate(settings); + } + } + + [Test] + public void ExternalDll_IsIgnoredUntilItsExactPathIsAuthorized() { ResetExternalRuntime(); var settings = CreateExternalSettings(); @@ -299,6 +315,62 @@ namespace ShrinkModFramework.Tests CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target); ShrinkModLoader.LoadAll(settings); + + Assert.IsEmpty(ShrinkModLoader.Mods); + Assert.AreEqual(0, + ShrinkModDiagnostics.CaptureExternalAssemblies(settings).CurrentRevisionCount); + + ShrinkModLoader.LoadAuthorized(settings, new[] { target }); + AssertExternalFixture("1.0.0", "v1"); + } + finally + { + CleanupExternalRuntime(settings, directory); + } + } + + [Test] + public void LegacyLoader_OnlyLoadsExplicitlyAuthorizedDllPaths() + { + ResetExternalRuntime(); + var settings = CreateExternalSettings(); + settings.useContextHost = false; + var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings); + var authorized = Path.Combine(directory, "Authorized.Mod.dll"); + var unauthorized = Path.Combine(directory, "Unauthorized.Mod.dll"); + + try + { + Directory.CreateDirectory(directory); + CopyFixture("ExternalFixture.Mod.V1.dll.bytes", authorized); + CopyFixture("ExternalFixture.Mod.V3.dll.bytes", unauthorized); + + ShrinkModLoader.LoadAuthorized(settings, new[] { authorized }); + + AssertExternalFixture("1.0.0", "v1"); + Assert.AreEqual(1, + ShrinkModDiagnostics.CaptureExternalAssemblies(settings).CurrentRevisionCount); + } + finally + { + CleanupExternalRuntime(settings, directory); + } + } + + [Test] + public void ExternalDllRevision_FailedReplacementRestoresPreviousAndBrokenBytesDoNotReplaceIt() + { + ResetExternalRuntime(); + var settings = CreateExternalSettings(); + var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings); + var target = Path.Combine(directory, "ExternalFixture.Mod.dll"); + + try + { + Directory.CreateDirectory(directory); + CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target); + + ShrinkModLoader.LoadAuthorized(settings, new[] { target }); AssertExternalFixture("1.0.0", "v1"); var restoredGeneration = ShrinkModLoader.Mods["external.fixture"].Generation; @@ -358,7 +430,7 @@ namespace ShrinkModFramework.Tests Assert.IsTrue(ShrinkModRuntimeDriver.InstanceForTesting!.HasWatcherForTesting); CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target); - ShrinkModLoader.LoadAll(settings); + ShrinkModLoader.LoadAuthorized(settings, new[] { target }); AssertExternalFixture("1.0.0", "v1"); // Two writes arrive as one debounce window; only the final valid revision should commit. @@ -395,7 +467,7 @@ namespace ShrinkModFramework.Tests CopyFixture("ExternalFixture.Provider.dll.bytes", providerPath); CopyFixture("ExternalFixture.Consumer.dll.bytes", consumerPath); - ShrinkModLoader.LoadAll(settings); + ShrinkModLoader.LoadAuthorized(settings, new[] { providerPath, consumerPath }); Assert.AreEqual(2, ShrinkModLoader.Mods.Count); Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.provider"].State); Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.consumer"].State); @@ -475,6 +547,7 @@ namespace ShrinkModFramework.Tests settings.useContextHost = true; settings.enableExternalDllMods = true; settings.autoCreateExternalModsDirectory = false; + settings.watchExternalModsDirectory = true; settings.externalModsFolderName = "ShrinkModFrameworkTests_" + Guid.NewGuid().ToString("N"); settings.assemblyNamePrefixes = new[] { "ExternalFixture." }; settings.enableHarmonyPatching = false; diff --git a/package.json b/package.json index d1e14c1..9d66009 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.cneicy.shrink-mod-framework", - "version": "0.2.2", + "version": "0.2.3", "displayName": "ShrinkModFramework", "description": "Unity 模组框架,提供发现、依赖、可逆生命周期、命名空间内容注册与优先级覆盖。", "unity": "2022.3",