using System.Buffers.Binary; using System.Net; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace MatrixImageEditor; public sealed class ImageConfig { [JsonPropertyName("schema_version")] public int SchemaVersion { get; set; } = 1; [JsonPropertyName("product")] public string Product { get; set; } = ""; [JsonPropertyName("software_version")] public string SoftwareVersion { get; set; } = ""; [JsonPropertyName("account")] public AccountConfig Account { get; set; } = new(); [JsonPropertyName("wifi")] public WifiConfig Wifi { get; set; } = new(); [JsonPropertyName("ipv4")] public Ipv4Config Ipv4 { get; set; } = new(); } public sealed class AccountConfig { [JsonPropertyName("username")] public string Username { get; set; } = ""; [JsonPropertyName("password")] public string Password { get; set; } = ""; } public sealed class WifiConfig { [JsonPropertyName("ssid")] public string Ssid { get; set; } = ""; [JsonPropertyName("password")] public string Password { get; set; } = ""; } public sealed class Ipv4Config { [JsonPropertyName("mode")] public string Mode { get; set; } = "dhcp"; [JsonPropertyName("address")] public string Address { get; set; } = ""; [JsonPropertyName("prefix")] public int Prefix { get; set; } [JsonPropertyName("gateway")] public string Gateway { get; set; } = ""; [JsonPropertyName("dns")] public List Dns { get; set; } = new(); } public static class ImageConfigCodec { public const int FileBytes = 64 * 1024; private const int HeaderBytes = 4096; private const int SlotBytes = (FileBytes - HeaderBytes) / 2; private const int SlotHeaderBytes = 52; private static readonly byte[] FileMagic = Encoding.ASCII.GetBytes("MSCCFG2\0"); private static readonly byte[] SlotMagic = Encoding.ASCII.GetBytes("MSCSLOT\0"); private static readonly Regex UsernamePattern = new("^[a-z_][a-z0-9_-]{0,31}$", RegexOptions.CultureInvariant); private static readonly Regex VersionPattern = new("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$", RegexOptions.CultureInvariant); private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = null, WriteIndented = false, UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, }; public static ImageConfig Deserialize(string json) { var config = JsonSerializer.Deserialize(json, JsonOptions) ?? throw new InvalidDataException("配置 JSON 为空"); Validate(config); return config; } public static string Serialize(ImageConfig config) { Validate(config); return JsonSerializer.Serialize(config, JsonOptions); } public static ImageConfig Read(byte[] data) => ReadDetailed(data).Config; public static (ImageConfig Config, ulong Generation, int Slot) ReadDetailed(byte[] data) { if (data.Length != FileBytes || !data.AsSpan(0, 8).SequenceEqual(FileMagic) || BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(8, 4)) != 1 || BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(12, 4)) != FileBytes) { throw new InvalidDataException("镜像配置区头部无效"); } var values = new List<(ImageConfig Config, ulong Generation, int Slot)>(); for (var slot = 0; slot < 2; slot++) { var offset = HeaderBytes + slot * SlotBytes; var span = data.AsSpan(offset, SlotBytes); if (!span[..8].SequenceEqual(SlotMagic)) continue; var generation = BinaryPrimitives.ReadUInt64LittleEndian(span.Slice(8, 8)); var length = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(16, 4)); if (length <= 0 || length > SlotBytes - SlotHeaderBytes) continue; var payload = span.Slice(SlotHeaderBytes, length).ToArray(); if (!SHA256.HashData(payload).AsSpan().SequenceEqual(span.Slice(20, 32))) continue; try { values.Add((Deserialize(Encoding.UTF8.GetString(payload)), generation, slot)); } catch { } } if (values.Count == 0) throw new InvalidDataException("镜像配置区没有可恢复的有效副本"); return values.OrderByDescending(value => value.Generation).ThenByDescending(value => value.Slot).First(); } public static (byte[] SlotData, int TargetSlot) EncodeUpdate(byte[] current, ImageConfig config) { Validate(config); var active = ReadDetailed(current); var target = 1 - active.Slot; var payload = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(config, JsonOptions) + "\n"); if (payload.Length > SlotBytes - SlotHeaderBytes) throw new InvalidDataException("配置内容过大"); var result = new byte[SlotBytes]; SlotMagic.CopyTo(result, 0); BinaryPrimitives.WriteUInt64LittleEndian(result.AsSpan(8, 8), active.Generation + 1); BinaryPrimitives.WriteInt32LittleEndian(result.AsSpan(16, 4), payload.Length); SHA256.HashData(payload).CopyTo(result, 20); payload.CopyTo(result, SlotHeaderBytes); return (result, target); } public static int SlotOffset(int slot) => HeaderBytes + slot * SlotBytes; public static void Validate(ImageConfig config) { if (config.Account is null || config.Wifi is null || config.Ipv4 is null) throw new InvalidDataException("镜像配置缺少必要部分"); if (config.SchemaVersion != 1 || config.Product != "matrix-screen-controller-walnutpi") throw new InvalidDataException("镜像产品或配置版本不受支持"); if (!VersionPattern.IsMatch(config.SoftwareVersion)) throw new InvalidDataException("软件版本格式无效"); if (!UsernamePattern.IsMatch(config.Account.Username)) throw new InvalidDataException("用户名只能使用小写字母、数字、下划线和连字符,且最长 32 个字符"); ValidateSecret(config.Account.Password, "账户密码", 8, 128); ValidateText(config.Wifi.Ssid, "Wi-Fi 名称", 1, 32); ValidateSecret(config.Wifi.Password, "Wi-Fi 密码", 8, 63); if (config.Ipv4.Mode == "dhcp") { config.Ipv4.Address = ""; config.Ipv4.Prefix = 0; config.Ipv4.Gateway = ""; config.Ipv4.Dns = new(); } else if (config.Ipv4.Mode == "static") { if (!IPAddress.TryParse(config.Ipv4.Address, out var address) || address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) throw new InvalidDataException("静态 IPv4 地址无效"); if (!IPAddress.TryParse(config.Ipv4.Gateway, out var gateway) || gateway.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) throw new InvalidDataException("静态 IPv4 网关无效"); if (config.Ipv4.Prefix is < 1 or > 32) throw new InvalidDataException("IPv4 前缀必须是 1 到 32"); if (!SameSubnet(address, gateway, config.Ipv4.Prefix)) throw new InvalidDataException("静态 IPv4 网关必须与地址处于同一子网"); if (config.Ipv4.Dns is null || config.Ipv4.Dns.Count is < 1 or > 4 || config.Ipv4.Dns.Any(item => !IPAddress.TryParse(item, out var parsed) || parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)) throw new InvalidDataException("DNS 必须包含 1 到 4 个 IPv4 地址"); } else throw new InvalidDataException("IPv4 模式无效"); } private static void ValidateSecret(string value, string label, int minimum, int maximum) { if (value is null || value.Length < minimum || value.Length > maximum || value.IndexOfAny(['\0', '\r', '\n']) >= 0) throw new InvalidDataException($"{label}长度或字符无效"); } private static void ValidateText(string value, string label, int minimum, int maximumBytes) { if (value is null || value.Length < minimum || Encoding.UTF8.GetByteCount(value) > maximumBytes || value.IndexOfAny(['\0', '\r', '\n']) >= 0) throw new InvalidDataException($"{label}长度或字符无效"); } private static bool SameSubnet(IPAddress address, IPAddress gateway, int prefix) { var addressValue = BinaryPrimitives.ReadUInt32BigEndian(address.GetAddressBytes()); var gatewayValue = BinaryPrimitives.ReadUInt32BigEndian(gateway.GetAddressBytes()); var mask = prefix == 32 ? uint.MaxValue : uint.MaxValue << (32 - prefix); return (addressValue & mask) == (gatewayValue & mask); } }