初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -0,0 +1,160 @@
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<string> 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<ImageConfig>(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);
}
}