using System.Buffers.Binary; using System.Globalization; using System.Security.Cryptography; using System.Text; namespace SatelliteProvider.Common.Utils; // AZ-503: pure-C# RFC 9562 (formerly RFC 4122 §4.3) UUIDv5 implementation. // // .NET 10 ships Guid.CreateVersion7 but NOT a version-5 builder, so we implement // the SHA-1-based algorithm here. Onboard `gps-denied-onboard/components/c6_tile_cache/_uuid.py` // MUST use the same TileNamespace constant and the same algorithm (Python's stdlib // uuid.uuid5 is identical by construction) so both sides of the wire compute // byte-identical IDs for the same (z, x, y, source, flight_id) inputs. // // Cross-repo namespace coordination: TileNamespace below is THE pinned value. // Any change here must be paired with the same change on the onboard side; the // AZ-503 task spec requires this and AC-1 (Python reference vectors) gates it. public static class Uuidv5 { // Pinned cross-repo namespace for tile identity. Must match // gps-denied-onboard `c6_tile_cache/_uuid.py:TILE_NAMESPACE`. // Chosen as a fresh random UUID (no semantic meaning beyond being a stable // 128-bit constant shared between the two repos). public static readonly Guid TileNamespace = new("5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c"); // AZ-505 consolidation: the canonical formula for a tile cell's // location_hash. Both TileRepository.GetByTileCoordinatesAsync and // TileService.GetInventoryAsync compute it; centralising here means the // cross-repo invariant (must byte-match gps-denied-onboard // `c6_tile_cache/_uuid.py:location_hash`) only has one source-of-truth in // this codebase. Format string is `"{z}/{x}/{y}"` under invariant culture — // matches the Python side's f-string output. public static Guid LocationHashForTile(int tileZoom, int tileX, int tileY) { var name = string.Create(CultureInfo.InvariantCulture, $"{tileZoom}/{tileX}/{tileY}"); return Create(TileNamespace, name); } public static Guid Create(Guid namespaceId, string name) { ArgumentNullException.ThrowIfNull(name); // Namespace UUIDs are concatenated as 16 bytes in network (big-endian) // order. .NET's Guid.ToByteArray() returns mixed-endian (RFC 4122 // "Microsoft" layout), so we cannot use it directly — we must rebuild // the byte array in big-endian order, matching what Python's // uuid.UUID.bytes produces. Span namespaceBytes = stackalloc byte[16]; WriteGuidBigEndian(namespaceId, namespaceBytes); var nameBytes = Encoding.UTF8.GetBytes(name); Span hash = stackalloc byte[20]; var buffer = new byte[16 + nameBytes.Length]; namespaceBytes.CopyTo(buffer); Buffer.BlockCopy(nameBytes, 0, buffer, 16, nameBytes.Length); if (!SHA1.TryHashData(buffer, hash, out _)) { throw new InvalidOperationException("SHA-1 hash computation failed."); } // Take first 16 bytes, set version to 5 (upper nibble of byte 6) and // variant to RFC 4122 (upper two bits of byte 8 set to `10`). Span uuidBytes = stackalloc byte[16]; hash[..16].CopyTo(uuidBytes); uuidBytes[6] = (byte)((uuidBytes[6] & 0x0F) | 0x50); uuidBytes[8] = (byte)((uuidBytes[8] & 0x3F) | 0x80); return ReadGuidBigEndian(uuidBytes); } private static void WriteGuidBigEndian(Guid value, Span destination) { Span mixed = stackalloc byte[16]; value.TryWriteBytes(mixed); // Convert from Microsoft mixed-endian (first 3 fields little-endian) to // network (big-endian) order. BinaryPrimitives.WriteUInt32BigEndian(destination[..4], BinaryPrimitives.ReadUInt32LittleEndian(mixed[..4])); BinaryPrimitives.WriteUInt16BigEndian(destination.Slice(4, 2), BinaryPrimitives.ReadUInt16LittleEndian(mixed.Slice(4, 2))); BinaryPrimitives.WriteUInt16BigEndian(destination.Slice(6, 2), BinaryPrimitives.ReadUInt16LittleEndian(mixed.Slice(6, 2))); mixed.Slice(8, 8).CopyTo(destination.Slice(8, 8)); } private static Guid ReadGuidBigEndian(ReadOnlySpan bigEndian) { Span mixed = stackalloc byte[16]; BinaryPrimitives.WriteUInt32LittleEndian(mixed[..4], BinaryPrimitives.ReadUInt32BigEndian(bigEndian[..4])); BinaryPrimitives.WriteUInt16LittleEndian(mixed.Slice(4, 2), BinaryPrimitives.ReadUInt16BigEndian(bigEndian.Slice(4, 2))); BinaryPrimitives.WriteUInt16LittleEndian(mixed.Slice(6, 2), BinaryPrimitives.ReadUInt16BigEndian(bigEndian.Slice(6, 2))); bigEndian.Slice(8, 8).CopyTo(mixed.Slice(8, 8)); return new Guid(mixed); } }