using System.Security.Cryptography; using System.Text; namespace MatrixImageEditor; public static class ImageOperations { private static readonly UTF8Encoding Utf8NoBomStrict = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); public static ImageConfig ReadConfig(string imagePath) { imagePath = ImagePathRules.ValidateExistingImage(imagePath); VerifySidecarIfPresent(imagePath); using var disk = new Fat16Disk(imagePath, writable: false); return ImageConfigCodec.Read(disk.ReadFile("MSCCFG.BIN")); } public static void WriteConfig(string imagePath, ImageConfig config) { imagePath = ImagePathRules.ValidateExistingImage(imagePath); VerifySidecarIfPresent(imagePath); using var disk = new Fat16Disk(imagePath, writable: true); var current = disk.ReadFile("MSCCFG.BIN"); if (ImageConfigCodec.Read(current).SoftwareVersion != config.SoftwareVersion) throw new InvalidDataException("禁止通过编辑器改变软件版本"); var update = ImageConfigCodec.EncodeUpdate(current, config); disk.WriteFileRange("MSCCFG.BIN", ImageConfigCodec.SlotOffset(update.TargetSlot), update.SlotData); var verified = ImageConfigCodec.Read(disk.ReadFile("MSCCFG.BIN")); if (!ImageConfigCodec.Serialize(verified).Equals(ImageConfigCodec.Serialize(config), StringComparison.Ordinal)) throw new IOException("配置写后校验失败"); } public static void WriteSha256(string imagePath, IProgress? progress) { imagePath = ImagePathRules.ValidateExistingImage(imagePath); using var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4 * 1024 * 1024, FileOptions.SequentialScan); using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); var buffer = new byte[4 * 1024 * 1024]; long completed = 0; int read; while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) { hash.AppendData(buffer, 0, read); completed += read; progress?.Report((int)(completed * 100 / stream.Length)); } var digest = Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant(); progress?.Report(100); var sidecar = imagePath + ".sha256"; var temporary = sidecar + "." + Guid.NewGuid().ToString("N") + ".tmp"; try { var bytes = Utf8NoBomStrict.GetBytes($"{digest} {Path.GetFileName(imagePath)}\n"); using (var output = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { output.Write(bytes); output.Flush(flushToDisk: true); } File.Move(temporary, sidecar, overwrite: true); var stored = ReadSidecarDigest(sidecar, Path.GetFileName(imagePath)); if (!stored.Equals(digest, StringComparison.Ordinal)) throw new IOException("镜像 SHA-256 摘要写后校验失败"); } finally { if (File.Exists(temporary)) File.Delete(temporary); } } private static void VerifySidecarIfPresent(string imagePath) { var sidecar = imagePath + ".sha256"; if (!File.Exists(sidecar)) return; var expected = ReadSidecarDigest(sidecar, Path.GetFileName(imagePath)); using var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4 * 1024 * 1024, FileOptions.SequentialScan); var actual = Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); if (!actual.Equals(expected, StringComparison.Ordinal)) throw new InvalidDataException("镜像内容与 SHA-256 摘要不符,禁止编辑"); } private static string ReadSidecarDigest(string sidecarPath, string expectedFileName) { if (new FileInfo(sidecarPath).Length > 128 * 1024) throw new InvalidDataException("镜像 SHA-256 摘要文件过大"); var bytes = File.ReadAllBytes(sidecarPath); if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) throw new InvalidDataException("镜像 SHA-256 摘要必须使用无 BOM UTF-8 格式"); string content; try { content = Utf8NoBomStrict.GetString(bytes); } catch (DecoderFallbackException exception) { throw new InvalidDataException("镜像 SHA-256 摘要不是有效的 UTF-8 文本", exception); } if (content.Length < 68 || content[^1] != '\n' || content.AsSpan(0, content.Length - 1).ContainsAny('\r', '\n')) { throw new InvalidDataException("镜像 SHA-256 摘要文件必须是以 LF 结尾的单行规范格式"); } var line = content.AsSpan(0, content.Length - 1); var digest = line[..64]; if (digest.ContainsAnyExcept("0123456789abcdef") || line[64] != ' ' || line[65] != ' ') { throw new InvalidDataException("镜像 SHA-256 摘要文件格式无效"); } var storedFileName = line[66..].ToString(); if (!storedFileName.Equals(expectedFileName, StringComparison.Ordinal)) { throw new InvalidDataException("镜像 SHA-256 摘要中的文件名与当前镜像不一致"); } return digest.ToString(); } }