open-standards/map/map-rs/crates/map-serve/src/lib.rs
Transaction Science ba1212361f Add aac, map, sandbox standards; catalog the standards family
Commit the previously-untracked standard directories and catalog them in the
root README alongside proof:

- aac/    — communication cargo class: portable boards + a ten-level
            offline-first cascade (Apache-2.0 code / CC-BY-4.0 spec).
- map/    — geospatial cargo class: range-requestable signed tilesets +
            an F0–F4 fidelity cascade (Apache-2.0 code / CC-BY-4.0 spec).
- sandbox/— isolated, metered, attested execution across the heterogeneous
            compute fabric (Apache-2.0 code / CC-BY-4.0 spec).
- jouleclaw-otel-authz, jouleclaw-trust — new jouleclaw reference crates.

Root README now lists aac, map, sandbox, and proof in the catalog, the tree,
and the contribution guide.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-07 17:04:36 -04:00

347 lines
13 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `map-serve` — a **transport-agnostic** tile-serving core for the Map standard.
//!
//! This is the protocol logic of a tile server with **no HTTP framework
//! dependency**: it parses a tile request, resolves it through the F0–F4
//! [`map_cascade`] against a caller-supplied store, and builds a content-
//! addressed [`Response`] carrying the tile bytes, a strong `ETag`, the closing
//! tier, and a **JCR-1 energy receipt** ([`map_receipt`]). Conditional requests
//! (`If-None-Match`) short-circuit to `304`.
//!
//! Binding it to a real socket is a thin wrapper the caller writes with whatever
//! transport they like (axum, hyper, a CGI shim, a service worker): construct an
//! [`HttpReq`] from the incoming request, call [`serve`], and map the returned
//! [`Response`] back. Everything here is pure and unit-testable without a network.
//!
//! ```
//! use map_serve::{serve, HttpReq, MemoryStore, ServedTile};
//! use map_schema::{Tier, AltBand, TimeBucket, Lod};
//! use map_index::cell_key;
//!
//! let mut store = MemoryStore::default();
//! let cell = cell_key(37.0, -122.0, 14, AltBand(0), TimeBucket::ATEMPORAL, Lod(0));
//! store.insert(&cell, Tier::Base, b"terrain-bytes".to_vec(), "application/octet-stream");
//!
//! let req = HttpReq { method: "GET", path: "/tiles/auto/37.0/-122.0/14",
//! if_none_match: None, sequence: 1, nonce: [0; 8] };
//! let res = serve(&store, "transaction.science", &req);
//! assert_eq!(res.status, 200);
//! assert_eq!(res.header("X-Map-Tier"), Some("F1"));
//! ```
pub mod dir;
pub use dir::DirStore;
use map_cascade::resolve;
pub use map_cascade::TileSource;
use map_index::cell_key;
use map_receipt::{Energy, Receipt};
use map_schema::{AltBand, CellKey, ContentId, Lod, Tier, TimeBucket};
/// Default S2 level when a request omits one (city-block scale).
pub const DEFAULT_LEVEL: u8 = 14;
/// Encoded tile bytes plus the MIME type a transport should send.
#[derive(Clone, Debug, PartialEq)]
pub struct ServedTile {
pub bytes: Vec<u8>,
pub content_type: String,
}
/// A tile store the server can both **resolve against** (via [`TileSource`]) and
/// **read bytes from**. A real implementation backs this with a tileset
/// directory, object storage, or a live feed.
pub trait TileServer: TileSource {
/// Fetch the encoded payload for a resolved content id.
fn fetch(&self, id: &ContentId) -> Option<ServedTile>;
}
/// A parsed tile request: a cell on the datum and a fidelity ceiling.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TileRequest {
pub lat: f64,
pub lon: f64,
pub level: u8,
pub tier: Tier,
}
/// A transport-neutral view of an incoming request — fill it from your framework.
#[derive(Clone, Copy, Debug)]
pub struct HttpReq<'a> {
pub method: &'a str,
pub path: &'a str,
/// The `If-None-Match` header value, if any (quotes optional).
pub if_none_match: Option<&'a str>,
/// Monotonic receipt sequence for this connection/steward.
pub sequence: u64,
/// Per-request entropy for the receipt nonce (the transport's to supply).
pub nonce: [u8; 8],
}
/// A transport-neutral response. Map `status`/`headers`/`body` onto your framework.
#[derive(Clone, Debug, PartialEq)]
pub struct Response {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl Response {
fn new(status: u16) -> Self {
Response { status, headers: Vec::new(), body: Vec::new() }
}
fn with(mut self, k: &str, v: impl Into<String>) -> Self {
self.headers.push((k.to_string(), v.into()));
self
}
/// Case-insensitive header lookup (convenience for callers and tests).
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
}
/// Parse a fidelity token: `f0`..`f4` (or their names), or `auto` for the full
/// cascade (a `Photoreal` ceiling — resolution falls back to whatever exists).
pub fn parse_tier(s: &str) -> Option<Tier> {
match s.to_ascii_lowercase().as_str() {
"auto" | "f4" | "photoreal" => Some(Tier::Photoreal),
"f3" | "feature" => Some(Tier::Feature),
"f2" | "field" => Some(Tier::Field),
"f1" | "base" => Some(Tier::Base),
"f0" | "cache" => Some(Tier::Cache),
_ => None,
}
}
/// Parse `\[/\]tiles/{tier}/{lat}/{lon}\[/{level}\]` (query string ignored).
pub fn parse_path(path: &str) -> Option<TileRequest> {
let path = path.split('?').next().unwrap_or(path);
let mut seg = path.split('/').filter(|s| !s.is_empty());
if seg.next()? != "tiles" {
return None;
}
let tier = parse_tier(seg.next()?)?;
let lat: f64 = seg.next()?.parse().ok()?;
let lon: f64 = seg.next()?.parse().ok()?;
if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
return None;
}
let level = match seg.next() {
Some(l) => l.parse().ok().filter(|&v| v <= 30)?,
None => DEFAULT_LEVEL,
};
Some(TileRequest { lat, lon, level, tier })
}
fn etag_matches(if_none_match: &str, etag: &str) -> bool {
let strip = |s: &str| s.trim().trim_start_matches("W/").trim_matches('"').to_string();
if if_none_match.trim() == "*" {
return true;
}
let target = strip(etag);
if_none_match.split(',').any(|c| strip(c) == target)
}
/// Handle one tile request: parse → cascade-resolve → content-addressed response
/// with a JCR-1 energy receipt. Pure; no I/O beyond the supplied store.
///
/// Status codes: `200` served, `304` not modified, `400` unparseable path,
/// `404` no tile (uncovered region or resolved id missing from the store),
/// `405` method not `GET`/`HEAD`.
pub fn serve<S: TileServer>(source: &S, steward: &str, req: &HttpReq) -> Response {
let head = req.method.eq_ignore_ascii_case("HEAD");
if !head && !req.method.eq_ignore_ascii_case("GET") {
return Response::new(405).with("Allow", "GET, HEAD");
}
let Some(tr) = parse_path(req.path) else {
return Response::new(400);
};
let cell = cell_key(tr.lat, tr.lon, tr.level, AltBand(0), TimeBucket::ATEMPORAL, Lod(0));
let res = match resolve(source, &cell, tr.tier) {
Ok(r) => r,
Err(_) => return Response::new(404),
};
let etag = format!("\"{}\"", res.content_id.to_hex());
if let Some(inm) = req.if_none_match {
if etag_matches(inm, &etag) {
return Response::new(304)
.with("ETag", etag)
.with("Cache-Control", "public, max-age=31536000, immutable");
}
}
let Some(served) = source.fetch(&res.content_id) else {
return Response::new(404);
};
let energy = Energy::estimate_tier(res.tier);
let receipt = Receipt {
cell,
content_id: res.content_id,
tier: res.tier,
energy,
sequence: req.sequence,
nonce: req.nonce,
};
let mut out = Response::new(200)
.with("Content-Type", served.content_type.clone())
.with("Content-Length", served.bytes.len().to_string())
.with("ETag", etag)
// Content-addressed payloads are immutable: cache forever.
.with("Cache-Control", "public, max-age=31536000, immutable")
.with("X-Map-Tier", res.tier.wire_tag())
.with("X-Map-Fell-Back", if res.fell_back { "1" } else { "0" })
.with("X-Map-Energy-Microjoules", energy.estimated_uj.to_string());
// The JCR-1 receipt as a compact single-line header (full bytes are the
// transport's to seal/sign with the steward key; this is the unsigned core).
if let Ok(wire) = receipt.to_jcr1_wire(steward) {
if let Ok(json) = serde_json::to_string(&wire) {
out = out.with("X-Map-Receipt", json);
}
}
out.body = if head { Vec::new() } else { served.bytes };
out
}
// ─────────────────────────────────────────────────────────────────────────
// A reference in-memory store — usable on its own and the basis for tests.
// ─────────────────────────────────────────────────────────────────────────
use std::collections::HashMap;
/// An in-memory [`TileServer`]: tiles keyed by `(s2 cell, tier)`, bytes keyed by
/// content id. The F0 cache short-circuit is a client concern, so [`cached`]
/// returns `None` here and the cascade resolves F1–F4.
///
/// [`cached`]: TileSource::cached
#[derive(Default)]
pub struct MemoryStore {
by_cell: HashMap<(u64, u8), ContentId>,
by_id: HashMap<[u8; 32], (Vec<u8>, String)>,
}
impl MemoryStore {
/// Insert a tile for `cell` at `tier`; returns its content id.
pub fn insert(&mut self, cell: &CellKey, tier: Tier, bytes: Vec<u8>, content_type: &str) -> ContentId {
let id = ContentId::of_bytes(&bytes);
self.by_cell.insert((cell.s2.0, tier.rank()), id);
self.by_id.insert(id.0, (bytes, content_type.to_string()));
id
}
}
impl TileSource for MemoryStore {
fn cached(&self, _cell: &CellKey) -> Option<ContentId> {
None
}
fn tile(&self, cell: &CellKey, tier: Tier) -> Option<ContentId> {
self.by_cell.get(&(cell.s2.0, tier.rank())).copied()
}
}
impl TileServer for MemoryStore {
fn fetch(&self, id: &ContentId) -> Option<ServedTile> {
self.by_id
.get(&id.0)
.map(|(bytes, ct)| ServedTile { bytes: bytes.clone(), content_type: ct.clone() })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cell_at(lat: f64, lon: f64) -> CellKey {
cell_key(lat, lon, DEFAULT_LEVEL, AltBand(0), TimeBucket::ATEMPORAL, Lod(0))
}
fn req<'a>(method: &'a str, path: &'a str) -> HttpReq<'a> {
HttpReq { method, path, if_none_match: None, sequence: 1, nonce: [7; 8] }
}
#[test]
fn parses_paths_and_rejects_bad_ones() {
let r = parse_path("/tiles/f2/37.5/-122.25").unwrap();
assert_eq!(r.tier, Tier::Field);
assert_eq!(r.level, DEFAULT_LEVEL);
assert_eq!(parse_path("/tiles/auto/0/0/9").unwrap().tier, Tier::Photoreal);
assert!(parse_path("/tiles/f2/91.0/0.0").is_none()); // lat out of range
assert!(parse_path("/tiles/nope/0/0").is_none()); // bad tier
assert!(parse_path("/health").is_none());
}
#[test]
fn serves_tile_with_receipt_and_etag() {
let mut store = MemoryStore::default();
let cell = cell_at(37.0, -122.0);
let id = store.insert(&cell, Tier::Base, b"terrain".to_vec(), "application/octet-stream");
let res = serve(&store, "transaction.science", &req("GET", "/tiles/auto/37.0/-122.0"));
assert_eq!(res.status, 200);
assert_eq!(res.body, b"terrain");
assert_eq!(res.header("X-Map-Tier"), Some("F1"));
assert_eq!(res.header("X-Map-Fell-Back"), Some("1")); // asked F4, got F1
assert_eq!(res.header("ETag"), Some(format!("\"{}\"", id.to_hex()).as_str()));
let receipt = res.header("X-Map-Receipt").unwrap();
assert!(receipt.contains("\"map_receipt\":\"map/1\""));
assert!(receipt.contains("\"tier\":\"F1\""));
assert!(receipt.contains("transaction.science"));
assert!(res.header("X-Map-Energy-Microjoules").is_some());
}
#[test]
fn picks_highest_tier_at_or_below_ceiling() {
let mut store = MemoryStore::default();
let cell = cell_at(10.0, 20.0);
store.insert(&cell, Tier::Base, b"f1".to_vec(), "x");
store.insert(&cell, Tier::Feature, b"f3".to_vec(), "x");
// Ceiling F4 → highest present is F3.
let r = serve(&store, "s", &req("GET", "/tiles/f4/10.0/20.0"));
assert_eq!(r.header("X-Map-Tier"), Some("F3"));
assert_eq!(r.body, b"f3");
// Ceiling F2 → must not return F3; falls to F1.
let r = serve(&store, "s", &req("GET", "/tiles/f2/10.0/20.0"));
assert_eq!(r.header("X-Map-Tier"), Some("F1"));
assert_eq!(r.body, b"f1");
}
#[test]
fn conditional_request_returns_304() {
let mut store = MemoryStore::default();
let cell = cell_at(1.0, 2.0);
let id = store.insert(&cell, Tier::Base, b"data".to_vec(), "x");
let etag = format!("\"{}\"", id.to_hex());
let mut r = req("GET", "/tiles/auto/1.0/2.0");
r.if_none_match = Some(&etag);
let res = serve(&store, "s", &r);
assert_eq!(res.status, 304);
assert!(res.body.is_empty());
assert_eq!(res.header("ETag"), Some(etag.as_str()));
}
#[test]
fn head_omits_body_keeps_headers() {
let mut store = MemoryStore::default();
let cell = cell_at(5.0, 5.0);
store.insert(&cell, Tier::Base, b"payload".to_vec(), "x");
let res = serve(&store, "s", &req("HEAD", "/tiles/auto/5.0/5.0"));
assert_eq!(res.status, 200);
assert!(res.body.is_empty());
assert_eq!(res.header("Content-Length"), Some("7"));
}
#[test]
fn uncovered_region_is_404_and_bad_method_405() {
let store = MemoryStore::default(); // empty: no base floor anywhere
assert_eq!(serve(&store, "s", &req("GET", "/tiles/auto/0.0/0.0")).status, 404);
assert_eq!(serve(&store, "s", &req("POST", "/tiles/auto/0.0/0.0")).status, 405);
assert_eq!(serve(&store, "s", &req("GET", "/nope")).status, 400);
}
}