// 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;
}
}