MigrationScriptHash.cs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. // Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
  2. //
  3. // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
  4. //
  5. // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
  6. using System.Security.Cryptography;
  7. namespace Admin.NET.Core.Update;
  8. /// <summary>
  9. /// 迁移脚本 SHA256 计算与跨平台行尾兼容校验。
  10. ///
  11. /// 背景:同一份 migration SQL 在 Windows(CRLF) 与 Linux/macOS(LF) 检出时字节不同,
  12. /// 直接对文件原始字节做 SHA256 会得到不同结果。于是「A 机器执行、B 机器启动」时,
  13. /// 守卫会把纯粹的行尾差异误判成脚本被篡改,导致 B 机器无法启动。
  14. ///
  15. /// 本类的取舍:
  16. /// · 只容忍行尾差异(CRLF / CR / LF 互转);
  17. /// · SQL 字符内容一旦发生任何变化(哪怕一个空格、一个 token),仍然判定为不一致;
  18. /// · 不做 Trim、不去空白、不去注释、不做 SQL 格式化。
  19. ///
  20. /// 新记录一律持久化 <see cref="ComputeCanonical"/>(LF 归一化)结果,
  21. /// 使后续任意平台执行同一文件都得到相同 hash;历史上已写入的
  22. /// raw-LF / raw-CRLF 记录继续被 <see cref="Matches"/> 接受,不需要回改数据库。
  23. /// </summary>
  24. [SuppressSniffer]
  25. public static class MigrationScriptHash
  26. {
  27. /// <summary>文件原始字节的 SHA256(历史记录可能是这个)。</summary>
  28. public static string ComputeRaw(byte[] content) => Sha256(content);
  29. /// <summary>行尾归一化为 LF 后的 SHA256。新记录一律用它,跨平台稳定。</summary>
  30. public static string ComputeCanonical(byte[] content) => Sha256(NormalizeToLf(content));
  31. /// <summary>行尾归一化为 CRLF 后的 SHA256(兼容历史上从 Windows 检出执行的记录)。</summary>
  32. public static string ComputeCrlf(byte[] content) => Sha256(ToCrlf(NormalizeToLf(content)));
  33. /// <summary>读取文件并返回 LF 归一化 SHA256。</summary>
  34. public static string ComputeCanonicalFromFile(string filePath) => ComputeCanonical(File.ReadAllBytes(filePath));
  35. /// <summary>
  36. /// 判断数据库中已记录的 hash 是否与当前文件内容等价。
  37. ///
  38. /// 命中 raw / LF / CRLF 三种之一即认为「SQL 内容相同,仅行尾不同」;
  39. /// 三者皆不命中说明脚本内容确实被改过,调用方应拒绝启动。
  40. /// </summary>
  41. public static bool Matches(string? storedHash, byte[] content)
  42. {
  43. if (string.IsNullOrWhiteSpace(storedHash)) return false;
  44. return Eq(storedHash, ComputeRaw(content))
  45. || Eq(storedHash, ComputeCanonical(content))
  46. || Eq(storedHash, ComputeCrlf(content));
  47. }
  48. /// <summary>文件版本的 <see cref="Matches(string, byte[])"/>。</summary>
  49. public static bool MatchesFile(string? storedHash, string filePath)
  50. => Matches(storedHash, File.ReadAllBytes(filePath));
  51. private static bool Eq(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
  52. private static string Sha256(byte[] bytes) => Convert.ToHexString(SHA256.HashData(bytes));
  53. /// <summary>CRLF → LF、独立 CR → LF;其余字节原样保留。</summary>
  54. private static byte[] NormalizeToLf(byte[] content)
  55. {
  56. const byte cr = 0x0D, lf = 0x0A;
  57. var output = new byte[content.Length];
  58. var length = 0;
  59. for (var i = 0; i < content.Length; i++)
  60. {
  61. var b = content[i];
  62. if (b == cr)
  63. {
  64. // CRLF 只写一个 LF;孤立 CR 也转成 LF。
  65. if (i + 1 < content.Length && content[i + 1] == lf) i++;
  66. output[length++] = lf;
  67. continue;
  68. }
  69. output[length++] = b;
  70. }
  71. return output.AsSpan(0, length).ToArray();
  72. }
  73. /// <summary>把已归一化为 LF 的内容转成 CRLF。</summary>
  74. private static byte[] ToCrlf(byte[] lfContent)
  75. {
  76. const byte cr = 0x0D, lf = 0x0A;
  77. var lfCount = 0;
  78. foreach (var b in lfContent)
  79. if (b == lf) lfCount++;
  80. var output = new byte[lfContent.Length + lfCount];
  81. var length = 0;
  82. foreach (var b in lfContent)
  83. {
  84. if (b == lf) output[length++] = cr;
  85. output[length++] = b;
  86. }
  87. return output;
  88. }
  89. }