using System.Buffers.Binary; using System.Buffers.Text; namespace WebDrop.Core; /// 16 字节标识(频道 id、条目 id),可作字典键。 public readonly struct Id16 : IEquatable { public const int Size = 16; private const int Base64Length = 22; private readonly ulong _hi; private readonly ulong _lo; private Id16(ulong hi, ulong lo) { _hi = hi; _lo = lo; } public static Id16 FromBytes(ReadOnlySpan bytes) { if (bytes.Length != Size) throw new ArgumentException("Id16 需要 16 字节", nameof(bytes)); return new Id16(BinaryPrimitives.ReadUInt64BigEndian(bytes), BinaryPrimitives.ReadUInt64BigEndian(bytes[8..])); } /// 只接受规范的 22 字符 base64url,拒绝同一值的非规范编码。 /// 32 个十六进制字符(文件服务和 blob 键里的写法) public static bool TryParseHex(string? hex, out Id16 id) { id = default; if (hex is not { Length: 32 }) return false; try { id = FromBytes(Convert.FromHexString(hex)); return true; } catch (FormatException) { return false; } } public static bool TryParse(ReadOnlySpan text, out Id16 id) { id = default; if (text.Length != Base64Length) return false; Span buf = stackalloc byte[Size]; if (!Codec.TryDecodeExact(text, buf)) return false; id = FromBytes(buf); return true; } public void WriteTo(Span destination) { BinaryPrimitives.WriteUInt64BigEndian(destination, _hi); BinaryPrimitives.WriteUInt64BigEndian(destination[8..], _lo); } public byte[] ToArray() { var bytes = new byte[Size]; WriteTo(bytes); return bytes; } public string ToBase64Url() => Codec.Encode(ToArray()); /// 小写十六进制。用于文件路径,避免大小写不敏感的文件系统造成不同 id 冲突。 public string ToHex() => Convert.ToHexStringLower(ToArray()); public bool Equals(Id16 other) => _hi == other._hi && _lo == other._lo; public override bool Equals(object? obj) => obj is Id16 other && Equals(other); public override int GetHashCode() => HashCode.Combine(_hi, _lo); public static bool operator ==(Id16 a, Id16 b) => a.Equals(b); public static bool operator !=(Id16 a, Id16 b) => !a.Equals(b); public override string ToString() => ToBase64Url(); } /// base64url(无填充)编解码,解码要求规范编码。 public static class Codec { public static string Encode(ReadOnlySpan bytes) => Base64Url.EncodeToString(bytes); /// 解码为恰好 destination.Length 字节,且重新编码后必须与输入逐字符一致。 public static bool TryDecodeExact(ReadOnlySpan text, Span destination) { // 注意:Base64Url.TryDecodeFromChars 遇到非法输入会抛 FormatException,这里用返回状态码的版本 var status = Base64Url.DecodeFromChars(text, destination, out int consumed, out int written, isFinalBlock: true); if (status != System.Buffers.OperationStatus.Done || consumed != text.Length || written != destination.Length) return false; Span check = stackalloc char[Base64Url.GetEncodedLength(destination.Length)]; return Base64Url.TryEncodeToChars(destination, check, out int n) && n == text.Length && check[..n].SequenceEqual(text); } public static bool TryDecodeExact(string? text, int length, out byte[] bytes) { bytes = []; if (string.IsNullOrEmpty(text) || text.Length != Base64Url.GetEncodedLength(length)) return false; var buf = new byte[length]; if (!TryDecodeExact(text.AsSpan(), buf)) return false; bytes = buf; return true; } }