mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 20:31:13 +00:00
34ee1e0b83
AZ-808: FluentValidation for POST /api/satellite/request - RegionRequestValidator: id non-empty, lat/lon/sizeMeters/zoomLevel ranges - RequestRegionRequest: [JsonRequired] on every property, no implicit defaults - Wired via .WithValidation<RequestRegionRequest>() in MapPost chain - Unit + integration tests + curl probe script - New contract: contracts/api/region-request.md v1.0.0 AZ-811: FluentValidation + envelope filter for GET /api/satellite/tiles/latlon - GetTileByLatLonQuery: nullable record (double?/int?) so the minimal-API binder never short-circuits with BadHttpRequestException before filters - GetTileByLatLonQueryValidator: Cascade(Stop) + NotNull + InclusiveBetween per param; missing surfaces as `\`<name>\` is required.` - RejectUnknownQueryParamsEndpointFilter: reusable IEndpointFilter that rejects any query key outside the allowed set with errors[<key>] map; catches legacy `?Latitude=` typos and hostile probes (`?debug=1&admin=1`) - Handler: [AsParameters] GetTileByLatLonQuery + .Value deref post-validator - Unit (validator + filter) + integration tests + curl probe script - New contract: contracts/api/tile-latlon.md v1.0.0 Shared hygiene - Promote AssertErrorsContainsMention from per-test-file private helpers to ProblemDetailsAssertions (closes batch-1 Low-severity DRY warning) - Sync Swagger param descriptions, README, blackbox/security/perf scripts, uuidv5 doc with the new lat/lon/zoom query-param names Docs - system-flows.md F1/F2 reference the new contracts + validation layers - modules/api_program.md adds Api/Validators + Api/DTOs sections - _autodev_state.md: batch 2 of 4 complete; next batch = AZ-809 All smoke tests green (mode=smoke, exit 0). AZ-808 + AZ-811 transitioned to In Testing on Jira. Co-authored-by: Cursor <cursoragent@cursor.com>
77 lines
5.3 KiB
Markdown
77 lines
5.3 KiB
Markdown
# Module: Common/Utils/Uuidv5
|
||
|
||
## Purpose
|
||
Deterministic UUIDv5 generator (RFC 9562 §5.5, SHA-1 namespace+name hashing) for tile identity. Pure C# implementation, ≤80 LoC, no third-party dependency. Owns the cross-repo `TileNamespace` constant that pins UUIDv5 outputs to be byte-identical between this workspace (C#) and the sibling `gps-denied-onboard` workspace (Python `uuid.uuid5`).
|
||
|
||
**csproj**: `SatelliteProvider.Common/Utils/Uuidv5.cs`
|
||
**Introduced**: AZ-503 (Cycle 5)
|
||
|
||
## Public Interface
|
||
|
||
All members are static on `Uuidv5`:
|
||
|
||
- `TileNamespace` (Guid, public const) — `5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c`. The shared namespace UUID used for every tile identity computation in this service and its onboard counterpart. **MUST NOT be changed** without coordinating a migration with `gps-denied-onboard/components/c6_tile_cache/_uuid.py`.
|
||
- `Create(Guid namespaceId, string name) → Guid` — produces a deterministic UUIDv5 by hashing `namespaceId.ToByteArrayBigEndian() || Encoding.UTF8.GetBytes(name)` with SHA-1, then assembling the 16 bytes per RFC 9562:
|
||
- bytes 0–3 are read as a big-endian uint32 (`time_low`)
|
||
- bytes 4–5 are read as a big-endian uint16 (`time_mid`)
|
||
- bytes 6–7 have their top 4 bits set to `0101` (version 5)
|
||
- byte 8 has its top 2 bits set to `10` (variant RFC 4122 / 9562)
|
||
- bytes 8–15 form the variant + clock_seq + node fields
|
||
- `Create(Guid namespaceId, ReadOnlySpan<byte> name) → Guid` — same as above but accepts a pre-encoded byte span; useful when the caller already has UTF-8 bytes or wants to avoid an intermediate string allocation.
|
||
|
||
## Internal Logic
|
||
|
||
- The .NET 10 `Guid.ToByteArray()` method emits the first three fields in little-endian (Microsoft historical behavior); RFC 9562 requires big-endian. The module uses a local `ToBigEndianByteArray(Guid)` helper that byte-swaps the first 4 bytes (time_low), the next 2 bytes (time_mid), and the next 2 bytes (time_hi_and_version) to produce the canonical big-endian layout before hashing. The same byte-swap is reversed when assembling the output `Guid` from the hash digest, so the in-memory `Guid` value still round-trips through `ToString()` to the expected hex form.
|
||
- SHA-1 is invoked via `SHA1.HashData(buffer)` (.NET 7+) which produces the 20-byte digest in one shot; only the first 16 bytes feed the resulting UUID (per RFC).
|
||
- The function is allocation-light for typical tile-key sizes: the hash input buffer is stack-allocated via `Span<byte>` when the namespace+name byte-length fits in 1024 bytes (always true for `{z}/{x}/{y}` and `{z}/{x}/{y}/{source}/{flight_id}` strings); larger payloads fall back to a pooled `byte[]`.
|
||
- The function is thread-safe (no shared mutable state).
|
||
|
||
## Reference Vectors
|
||
|
||
`SatelliteProvider.Tests/Uuidv5Tests.cs` pins 10 reference vectors generated by Python (`uuid.uuid5(TILE_NAMESPACE, name)`). Each vector pairs an input `name` with the expected `Guid` string. The C# implementation must produce byte-identical output. Two representative pairs:
|
||
|
||
| Name | Expected UUIDv5 |
|
||
|------|-----------------|
|
||
| `"18/12345/23456"` | `38b26f49-a966-5121-aaf4-9cc476f57869` |
|
||
| `"18/12345/23456/google_maps/00000000-0000-0000-0000-000000000000"` | `e228d1aa-25d4-556e-a72d-e0484756e165` |
|
||
|
||
The second value is observable end-to-end: a fresh `GET /api/satellite/tiles/latlon?lat=47.461747&lon=37.647063&zoom=18` returns `tileId = e228d1aa-25d4-556e-a72d-e0484756e165` because `(47.461747, 37.647063)` maps to slippy `(z=18, x=158485, y=91707)` — and the integration test asserts that exact value. (AZ-811 cycle 8 renamed the query params `Latitude/Longitude/ZoomLevel` → `lat/lon/zoom` for OSM consistency.)
|
||
|
||
## Dependencies
|
||
|
||
- `System.Security.Cryptography.SHA1`
|
||
- `System.Buffers.Binary.BinaryPrimitives` (for big-endian byte-swaps)
|
||
- `System.Buffers.ArrayPool<byte>` (for the >1024-byte fallback path)
|
||
|
||
No third-party packages. No NuGet additions for AZ-503.
|
||
|
||
## Consumers
|
||
|
||
- `SatelliteProvider.Services.TileDownloader.TileService.BuildTileEntity` — computes `Id` and `LocationHash` for every newly downloaded Google Maps tile.
|
||
- `SatelliteProvider.Services.TileDownloader.UavTileUploadHandler.PersistAsync` — computes `Id` and `LocationHash` for every UAV upload.
|
||
- `SatelliteProvider.IntegrationTests.UavUploadTests` — seeds `location_hash` values via raw SQL when bypassing the application code path.
|
||
- `SatelliteProvider.IntegrationTests.MigrationTests` — generates expected UUIDv5 outputs to validate migration 014's `pg_temp.uuidv5` PL/pgSQL backfill function.
|
||
|
||
## Data Models
|
||
|
||
Operates only on `Guid` and `string` / `Span<byte>`. No persistence model.
|
||
|
||
## Configuration
|
||
|
||
None. The namespace constant is pinned in source.
|
||
|
||
## External Integrations
|
||
|
||
None (pure computation).
|
||
|
||
## Security
|
||
|
||
The function is deterministic by design — it is NOT a cryptographic hash for security purposes. Two callers with the same `(namespace, name)` will always produce the same output. Treat the result as a content/location handle, not a secret. SHA-1 is used for RFC 9562 compatibility, not for collision resistance against an adversary.
|
||
|
||
## Tests
|
||
|
||
`SatelliteProvider.Tests/Uuidv5Tests.cs`:
|
||
- `Create_MatchesPythonReferenceVectors_AC1` — 10 reference vectors (AZ-503 AC-1).
|
||
- `Create_IsDeterministic` — re-running with the same inputs returns the same `Guid`.
|
||
- `Create_SetsVersionAndVariantBits` — asserts the version nibble is `5` and the variant top-2-bits are `10`.
|