using System; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using DCIT.Core.Utilities; namespace DCIT.Core.Services { public class BmpDiffCodec { private const string Magic = "DCITBMP1"; private const ushort HeaderVersion = 1; private const ushort Flags = 0; private const int FixedWidth = 1024; private const int HeaderSize = 8 + 2 + 2 + 8 + 32; public void EncodeJson(string canonicalJson, string outputPath) { var payloadBytes = Encoding.UTF8.GetBytes(canonicalJson ?? string.Empty); using (var stream = new MemoryStream(payloadBytes)) { EncodeFromStream(stream, outputPath); } } public void EncodeFromStream(Stream jsonStream, string outputPath) { if (jsonStream == null) { throw new ArgumentNullException("jsonStream"); } byte[] hash; long length; if (jsonStream.CanSeek) { length = jsonStream.Length; using (var sha = SHA256.Create()) { hash = sha.ComputeHash(jsonStream); } jsonStream.Position = 0; } else { using (var buffered = new MemoryStream()) { jsonStream.CopyTo(buffered); length = buffered.Length; using (var sha = SHA256.Create()) { hash = sha.ComputeHash(buffered); } buffered.Position = 0; WriteBitmapWithPayload(outputPath, length, hash, buffered); return; } } WriteBitmapWithPayload(outputPath, length, hash, jsonStream); } public string DecodeJson(string bmpPath) { using (var bitmap = new Bitmap(bmpPath)) { var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); var bitmapData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, bitmap.PixelFormat); try { var rowLength = bitmap.Width; var pixelBytes = new byte[rowLength * bitmap.Height]; for (var row = 0; row < bitmap.Height; row++) { var source = IntPtr.Add(bitmapData.Scan0, row * bitmapData.Stride); Marshal.Copy(source, pixelBytes, row * rowLength, rowLength); } var payloadBytes = ParseBlob(pixelBytes); return Encoding.UTF8.GetString(payloadBytes); } finally { bitmap.UnlockBits(bitmapData); } } } private static void WriteBitmapWithPayload(string outputPath, long payloadLength, byte[] payloadHash, Stream payloadStream) { var totalBytes = HeaderSize + payloadLength; var height = Math.Max(1, (int)Math.Ceiling(totalBytes / (double)FixedWidth)); var pixelCount = FixedWidth * height; var pixelBytes = new byte[pixelCount]; var offset = 0; var magicBytes = Encoding.ASCII.GetBytes(Magic); Buffer.BlockCopy(magicBytes, 0, pixelBytes, offset, magicBytes.Length); offset += magicBytes.Length; pixelBytes[offset++] = (byte)(HeaderVersion & 0xFF); pixelBytes[offset++] = (byte)((HeaderVersion >> 8) & 0xFF); pixelBytes[offset++] = (byte)(Flags & 0xFF); pixelBytes[offset++] = (byte)((Flags >> 8) & 0xFF); pixelBytes[offset++] = (byte)(payloadLength & 0xFF); pixelBytes[offset++] = (byte)((payloadLength >> 8) & 0xFF); pixelBytes[offset++] = (byte)((payloadLength >> 16) & 0xFF); pixelBytes[offset++] = (byte)((payloadLength >> 24) & 0xFF); pixelBytes[offset++] = (byte)((payloadLength >> 32) & 0xFF); pixelBytes[offset++] = (byte)((payloadLength >> 40) & 0xFF); pixelBytes[offset++] = (byte)((payloadLength >> 48) & 0xFF); pixelBytes[offset++] = (byte)((payloadLength >> 56) & 0xFF); Buffer.BlockCopy(payloadHash, 0, pixelBytes, offset, 32); offset += 32; var buffer = new byte[65536]; int read; while ((read = payloadStream.Read(buffer, 0, buffer.Length)) > 0) { if (offset + read > pixelBytes.Length) { throw new InvalidDataException("BMP 载荷长度超出预期像素缓冲。"); } Buffer.BlockCopy(buffer, 0, pixelBytes, offset, read); offset += read; } if (offset != totalBytes) { throw new InvalidDataException("BMP 载荷长度与声明的长度不一致。"); } using (var bitmap = new Bitmap(FixedWidth, height, PixelFormat.Format8bppIndexed)) { var palette = bitmap.Palette; for (var index = 0; index < 256; index++) { palette.Entries[index] = Color.FromArgb(index, index, index); } bitmap.Palette = palette; var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); try { for (var row = 0; row < height; row++) { var sourceOffset = row * FixedWidth; var destination = IntPtr.Add(bitmapData.Scan0, row * bitmapData.Stride); Marshal.Copy(pixelBytes, sourceOffset, destination, FixedWidth); } } finally { bitmap.UnlockBits(bitmapData); } Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? string.Empty); bitmap.Save(outputPath, ImageFormat.Bmp); } } private static byte[] BuildBlob(byte[] payloadBytes) { payloadBytes = payloadBytes ?? new byte[0]; var payloadHash = Convert.FromBase64String(HashUtility.ComputeSha256Base64(payloadBytes)); using (var stream = new MemoryStream()) using (var writer = new BinaryWriter(stream, Encoding.ASCII)) { writer.Write(Encoding.ASCII.GetBytes(Magic)); writer.Write(HeaderVersion); writer.Write(Flags); writer.Write((ulong)payloadBytes.Length); writer.Write(payloadHash); writer.Write(payloadBytes); writer.Flush(); return stream.ToArray(); } } private static byte[] ParseBlob(byte[] blobBytes) { using (var stream = new MemoryStream(blobBytes)) using (var reader = new BinaryReader(stream, Encoding.ASCII)) { var magic = Encoding.ASCII.GetString(reader.ReadBytes(8)); if (!string.Equals(magic, Magic, StringComparison.Ordinal)) { throw new InvalidDataException("BMP 差异文件魔数无效。"); } var version = reader.ReadUInt16(); if (version != HeaderVersion) { throw new InvalidDataException("BMP 差异文件版本不受支持。"); } _ = reader.ReadUInt16(); var payloadLength = reader.ReadUInt64(); var payloadHash = reader.ReadBytes(32); var payloadBytes = reader.ReadBytes((int)payloadLength); var actualHash = Convert.FromBase64String(HashUtility.ComputeSha256Base64(payloadBytes)); if (!BytesEqual(payloadHash, actualHash)) { throw new InvalidDataException("BMP 差异文件载荷校验失败。"); } return payloadBytes; } } private static bool BytesEqual(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } for (var index = 0; index < left.Length; index++) { if (left[index] != right[index]) { return false; } } return true; } } }