// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 // // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 // // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! using System.Security.Cryptography; namespace Admin.NET.Core.Update; /// /// 迁移脚本 SHA256 计算与跨平台行尾兼容校验。 /// /// 背景:同一份 migration SQL 在 Windows(CRLF) 与 Linux/macOS(LF) 检出时字节不同, /// 直接对文件原始字节做 SHA256 会得到不同结果。于是「A 机器执行、B 机器启动」时, /// 守卫会把纯粹的行尾差异误判成脚本被篡改,导致 B 机器无法启动。 /// /// 本类的取舍: /// · 只容忍行尾差异(CRLF / CR / LF 互转); /// · SQL 字符内容一旦发生任何变化(哪怕一个空格、一个 token),仍然判定为不一致; /// · 不做 Trim、不去空白、不去注释、不做 SQL 格式化。 /// /// 新记录一律持久化 (LF 归一化)结果, /// 使后续任意平台执行同一文件都得到相同 hash;历史上已写入的 /// raw-LF / raw-CRLF 记录继续被 接受,不需要回改数据库。 /// [SuppressSniffer] public static class MigrationScriptHash { /// 文件原始字节的 SHA256(历史记录可能是这个)。 public static string ComputeRaw(byte[] content) => Sha256(content); /// 行尾归一化为 LF 后的 SHA256。新记录一律用它,跨平台稳定。 public static string ComputeCanonical(byte[] content) => Sha256(NormalizeToLf(content)); /// 行尾归一化为 CRLF 后的 SHA256(兼容历史上从 Windows 检出执行的记录)。 public static string ComputeCrlf(byte[] content) => Sha256(ToCrlf(NormalizeToLf(content))); /// 读取文件并返回 LF 归一化 SHA256。 public static string ComputeCanonicalFromFile(string filePath) => ComputeCanonical(File.ReadAllBytes(filePath)); /// /// 判断数据库中已记录的 hash 是否与当前文件内容等价。 /// /// 命中 raw / LF / CRLF 三种之一即认为「SQL 内容相同,仅行尾不同」; /// 三者皆不命中说明脚本内容确实被改过,调用方应拒绝启动。 /// public static bool Matches(string? storedHash, byte[] content) { if (string.IsNullOrWhiteSpace(storedHash)) return false; return Eq(storedHash, ComputeRaw(content)) || Eq(storedHash, ComputeCanonical(content)) || Eq(storedHash, ComputeCrlf(content)); } /// 文件版本的 public static bool MatchesFile(string? storedHash, string filePath) => Matches(storedHash, File.ReadAllBytes(filePath)); private static bool Eq(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase); private static string Sha256(byte[] bytes) => Convert.ToHexString(SHA256.HashData(bytes)); /// CRLF → LF、独立 CR → LF;其余字节原样保留。 private static byte[] NormalizeToLf(byte[] content) { const byte cr = 0x0D, lf = 0x0A; var output = new byte[content.Length]; var length = 0; for (var i = 0; i < content.Length; i++) { var b = content[i]; if (b == cr) { // CRLF 只写一个 LF;孤立 CR 也转成 LF。 if (i + 1 < content.Length && content[i + 1] == lf) i++; output[length++] = lf; continue; } output[length++] = b; } return output.AsSpan(0, length).ToArray(); } /// 把已归一化为 LF 的内容转成 CRLF。 private static byte[] ToCrlf(byte[] lfContent) { const byte cr = 0x0D, lf = 0x0A; var lfCount = 0; foreach (var b in lfContent) if (b == lf) lfCount++; var output = new byte[lfContent.Length + lfCount]; var length = 0; foreach (var b in lfContent) { if (b == lf) output[length++] = cr; output[length++] = b; } return output; } }