Files
Oleksandr Bezdieniezhnykh 34ee1e0b83 [AZ-808] [AZ-811] Strict validation on region POST + lat/lon GET
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>
2026-05-22 16:29:41 +03:00

5.3 KiB
Raw Permalink Blame History

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 03 are read as a big-endian uint32 (time_low)
    • bytes 45 are read as a big-endian uint16 (time_mid)
    • bytes 67 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 815 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/ZoomLevellat/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.