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