using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Media;
using TeamAAS;
namespace TeamAAS.Theme
{
///
/// 应用级主题资源守护:防御第三方控件(海康 VM 系列,经 WindowsFormsHost/ElementHost 进入视觉树)
/// 对 Application.Resources 的越界写入。
///
/// 背景故障(2026-09-04):
/// 打开"VM 工具块"编辑视图后切换明暗主题,齿轮 Popup 布局时抛
/// InvalidOperationException:"#FF797979"不是属性"Color"的有效值,且被全局异常处理器
/// Handled=true 后每帧重爆、日志刷屏(10MB/5min)。
///
/// 根因(日志 + 反编译确认):
/// 1. 某第三方控件把应用级主题同名键(XxxColor/XxxBrush 形态)写成了字符串 "#FF797979"
/// (Color.ToString() 的产物;所有 VM DLL 静态字节里均无此字面量,纯运行时生成)。
/// 写入资源字典不做类型检查,因此污染当时不报错。
/// 2. HandyControl 的画刷是 <SolidColorBrush Color="{DynamicResource XxxColor}"/> 结构;
/// 画刷 Color 表达式在重新求值时把污染的字符串求进有效值(表达式值延迟类型检查)。
/// 3. Border.ArrangeOverride 读取画刷 Color 时类型检查失败才抛异常。
/// 4. VMControls.Winform.Release 的控件只是 ElementHost 壳,内部仍是 VMControls.WPF 的
/// WPF 控件(反编译确认),所以"改用 WinForms 版避免污染"无效。
///
/// 防御体系(切主题入口消毒,单道防线):
/// - ThemeManager 切主题入口消毒:切换/基准捕获前 Purge 第三方注入字典与污染条目,保证求值链纯净;
/// - 结构损坏 / 坏画刷 / 基准缺失时 ForceReplaceTheme 整替官方字典兜底。
/// 注:常驻 Watchdog 轮询与未处理异常急救已移除——ThemeOverlay 纯值覆盖层对污染天然免疫。
///
internal static class VmResourceGuard
{
/// 颜色字面量:#RRGGBB 或 #AARRGGBB(污染值的典型形态)
private static readonly Regex ColorLiteralRegex =
new Regex(@"^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", RegexOptions.Compiled);
/// 官方主题字典识别标记:pack Source 中含此串才允许触碰/保留
private const string OfficialThemeMarker = "HandyControl";
///
/// 仅消毒:移除应用级资源中"字符串形式颜色"的污染条目。
/// 供切主题入口(整替前)调用——污染清干净后,画刷求值才安全。
///
public static void Sanitize(string trigger)
{
ScanAll(trigger);
}
///
/// 检查主题字典中是否存在坏画刷(Color 有效值已被字符串污染、读取即爆)。
/// 供切主题判断:即使字典结构标准,画刷坏了也必须强制整替。
///
public static bool HasBrokenBrush()
{
var app = Application.Current;
if (app == null) return false;
bool broken = false;
try
{
ScanDict(app.Resources, "HealthCheck", "HealthCheck", ref broken);
}
catch (Exception ex)
{
AppLogger.Error("[资源守护:HealthCheck] 扫描失败", ex, nameof(VmResourceGuard));
}
return broken;
}
private static void ScanAll(string trigger)
{
var app = Application.Current;
if (app == null) return;
try
{
// 第一步:移除第三方控件注入应用合并字典的非 HC 字典(含坏 BAML/字符串颜色键,
// 是画刷求值链被击穿的污染源)
PurgeForeignMergedDictionaries(trigger);
bool brushBroken = false;
int removed = ScanDict(app.Resources, "Application.Resources", trigger, ref brushBroken);
if (removed > 0)
{
AppLogger.Warning(
$"[资源守护:{trigger}] 已清除 {removed} 个污染条目(明细见上方日志)",
nameof(VmResourceGuard));
}
}
catch (Exception ex)
{
AppLogger.Error($"[资源守护:{trigger}] 扫描失败", ex, nameof(VmResourceGuard));
}
}
///
/// 急救/结构修复:强制整替 HandyControl 明暗双字典(调色板 + 画刷/样式),
/// 用全新画刷实例替换所有被污染写坏的旧画刷。
/// 正常切主题走覆盖层重建(见 ThemeManager),本方法仅在
/// 「字典结构损坏」「基准未捕获」「检测到坏画刷」时由守护/异常钩子调用。
///
/// 目标明暗;null 时沿用 ThemeOverlay 记录的当前明暗(守护轮询路径)。
public static void ForceReplaceTheme(bool? dark = null)
{
var merged = Application.Current.Resources.MergedDictionaries;
bool isDark = dark.HasValue ? dark.Value : ThemeOverlay.CurrentDark;
// 先摘除覆盖层:保证整替与基准捕获读到的是官方字典真值(覆盖层查找优先级最高)
ThemeOverlay.RemoveOverlay();
// 消毒:移除第三方注入字典/污染条目(Watchdog/异常钩子可能不经 Sanitize 直达此处),
// 保证基准捕获走的是纯净求值链
Sanitize("ForceReplaceTheme");
var skinUri = new Uri(isDark
? "pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml"
: "pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml");
var themeUri = new Uri("pack://application:,,,/HandyControl;component/Themes/Theme.xaml");
// 1) 调色板槽位
int skinIdx = FindSlot(merged, d => d.Source != null &&
(d.Source.OriginalString.Contains("SkinDefault.xaml") ||
d.Source.OriginalString.Contains("SkinDark.xaml")));
if (skinIdx < 0) skinIdx = 0;
merged[skinIdx] = new ResourceDictionary { Source = skinUri };
// 2) 画刷+样式槽位:Source 含 Theme.xaml,或旧方案残留的 Theme 实例(Source=null)
int themeIdx = FindSlot(merged, d => d is HandyControl.Themes.Theme ||
(d.Source != null && d.Source.OriginalString.Contains("Theme.xaml")));
if (themeIdx < 0) themeIdx = merged.Count > 1 ? 1 : merged.Count;
var themeDict = new ResourceDictionary { Source = themeUri };
if (themeIdx >= merged.Count) merged.Add(themeDict);
else merged[themeIdx] = themeDict;
// 重新捕获该明暗的官方基准(此时求值链干净)并重建覆盖层(纯值画刷,免疫污染)
ThemeOverlay.CaptureBase(isDark);
ThemeOverlay.Apply(isDark, ThemeOverlay.CurrentAccent);
}
///
/// 移除应用合并字典中"非白名单"的字典(第三方控件框架级注入的字典:
/// 实测含坏 BAML(GradientStop.Color 解析即爆)与字符串形式颜色键,它们挂在
/// Application 求值链上会让所有 DynamicResource 画刷被击穿)。
/// 白名单字典保留;第三方控件自身的资源需求由其元素级资源机制解决。
///
private static void PurgeForeignMergedDictionaries(string trigger)
{
var merged = Application.Current.Resources.MergedDictionaries;
// 先收集后移除,避免枚举中改集合
List foreign = null;
for (int i = 0; i < merged.Count; i++)
{
var dict = merged[i];
if (IsOfficialThemeDict(dict)) continue;
if (ThemeOverlay.IsOverlay(dict)) continue; // 主题覆盖层(我们自己的纯值字典)放行
string describe = Describe(dict);
if (string.IsNullOrEmpty(describe)) describe = "(Source=null)";
AppLogger.Warning(
$"[资源守护:{trigger}] 移除第三方注入的应用级字典 md[{i}] {describe}",
nameof(VmResourceGuard));
if (foreign == null) foreign = new List();
foreign.Add(dict);
}
if (foreign != null)
{
foreach (var dict in foreign)
{
try { merged.Remove(dict); }
catch { /* 无法移除时忽略(下一轮再试) */ }
}
}
}
/// 是否官方主题字典(Source 含 HandyControl 标记;Source=null 不算)。
private static bool IsOfficialThemeDict(ResourceDictionary dict)
{
try
{
var src = dict.Source;
return src != null && src.OriginalString.Contains(OfficialThemeMarker);
}
catch
{
return false;
}
}
///
/// 递归扫描字典树:移除污染条目(先收集后移除,避免枚举中改集合),
/// 顺带用 try 读画刷 Color 检测坏画刷。返回本次移除的条目数。
/// 只递归白名单字典——第三方字典含坏 BAML(展开即爆),触碰只会制造
/// 第一次机会异常并中断扫描,其中的污染由 PurgeForeignMergedDictionaries 整体移除。
///
private static int ScanDict(ResourceDictionary dict, string path, string trigger, ref bool brushBroken)
{
int removed = 0;
// 1) 先扫子合并字典(仅白名单字典;第三方字典不触碰)
var merged = dict.MergedDictionaries;
for (int i = 0; i < merged.Count; i++)
{
var child = merged[i];
if (!IsOfficialThemeDict(child)) continue;
string childPath = path + "/md[" + i + "]" + Describe(child);
ScanDict(child, childPath, trigger, ref brushBroken);
}
// 2) 扫描本字典条目
List