using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace FrameProcessor.Domain; /// /// A 48-bit MAC address. Canonical form is lowercase hex without separators (e.g. aabbccddeeff); /// parsing accepts colon, hyphen, or unseparated hex with arbitrary surrounding whitespace. /// [JsonConverter(typeof(MacAddressJsonConverter))] public readonly record struct MacAddress : IParsable, ISpanParsable { private readonly byte _b0; private readonly byte _b1; private readonly byte _b2; private readonly byte _b3; private readonly byte _b4; private readonly byte _b5; public MacAddress(ReadOnlySpan bytes) { if (bytes.Length != 6) { throw new ArgumentException("MAC address must be exactly 6 bytes.", nameof(bytes)); } _b0 = bytes[0]; _b1 = bytes[1]; _b2 = bytes[2]; _b3 = bytes[3]; _b4 = bytes[4]; _b5 = bytes[5]; } public override string ToString() => string.Create(12, this, static (span, mac) => { WriteHex(span[..2], mac._b0); WriteHex(span.Slice(2, 2), mac._b1); WriteHex(span.Slice(4, 2), mac._b2); WriteHex(span.Slice(6, 2), mac._b3); WriteHex(span.Slice(8, 2), mac._b4); WriteHex(span.Slice(10, 2), mac._b5); }); public static bool TryParse(string? s, out MacAddress result) { if (s is null) { result = default; return false; } return TryParse(s.AsSpan(), out result); } public static bool TryParse(ReadOnlySpan s, out MacAddress result) { result = default; s = s.Trim(); if (s.IsEmpty) { return false; } Span hex = stackalloc char[12]; var written = 0; foreach (var c in s) { if (c is ':' or '-') { continue; } if (written == 12) { return false; } if (!IsHexDigit(c)) { return false; } hex[written++] = c; } if (written != 12) { return false; } Span bytes = stackalloc byte[6]; for (var i = 0; i < 6; i++) { if (!byte.TryParse(hex.Slice(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b)) { return false; } bytes[i] = b; } result = new MacAddress(bytes); return true; } public static MacAddress Parse(string s) { ArgumentNullException.ThrowIfNull(s); return TryParse(s, out var result) ? result : throw new FormatException($"'{s}' is not a valid MAC address."); } public static MacAddress Parse(string s, IFormatProvider? provider) => Parse(s); public static bool TryParse(string? s, IFormatProvider? provider, out MacAddress result) => TryParse(s, out result); public static MacAddress Parse(ReadOnlySpan s, IFormatProvider? provider) => TryParse(s, out var result) ? result : throw new FormatException($"'{s}' is not a valid MAC address."); public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out MacAddress result) => TryParse(s, out result); private static bool IsHexDigit(char c) => (uint)(c - '0') <= 9 || (uint)(c - 'a') <= 5 || (uint)(c - 'A') <= 5; private static void WriteHex(Span dest, byte value) { dest[0] = HexChar(value >> 4); dest[1] = HexChar(value & 0xF); } private static char HexChar(int nibble) => (char)(nibble < 10 ? '0' + nibble : 'a' + (nibble - 10)); } internal sealed class MacAddressJsonConverter : JsonConverter { public override MacAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var s = reader.GetString(); if (!MacAddress.TryParse(s, out var mac)) { throw new JsonException($"'{s}' is not a valid MAC address."); } return mac; } public override void Write(Utf8JsonWriter writer, MacAddress value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString()); }