use crate::{Error, Kind, Limits, is_identifier};
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, BTreeSet},
fmt::Write as _,
ops::Bound,
};
use tilewright_jsonc::{Document, Limits as ParseLimits, write_json_string};
use tilewright_pack::{Identity, Pack};
use tilewright_source::World;
pub(crate) type Fingerprint = [u8; 32];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Input {
Source,
Pack,
}
impl Input {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Source => "source",
Self::Pack => "pack",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Side {
pub input: Input,
pub profile: String,
pub profile_version: String,
pub source_digest: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Snapshot {
pub(crate) side: Side,
pub(crate) ids: BTreeMap<Kind, BTreeMap<String, Fingerprint>>,
pub(crate) object_planes: BTreeMap<String, String>,
}
impl Snapshot {
pub fn from_world(world: &World, limits: &Limits) -> Result<Self, Error> {
let profile = world.profile();
let mut builder = Builder::new(*limits);
let mut read = || -> Result<(), String> {
for (position, name) in profile.render_layers.iter().enumerate() {
builder.render_layer(name, position)?;
}
for (id, plane) in world.planes() {
builder.plane(id, &plane.canonical)?;
for (zone, entry) in &plane.zones {
builder.zone(id, zone, &entry.canonical)?;
}
for (anchor, entry) in &plane.anchors {
builder.anchor(id, anchor, entry.layer, entry.at)?;
}
for (stairwell, entry) in &plane.stairwells {
builder.stairwell(id, stairwell, &entry.canonical)?;
}
for (layer, cx, cy) in plane.chunks.keys() {
builder.chunk(id, *layer, *cx, *cy)?;
}
}
for (uid, location) in world.object_uids() {
builder.object(uid, &location.plane)?;
}
for ((kind, id), definition) in world.definitions() {
builder.definition(kind, id, &definition.canonical)?;
}
Ok(())
};
let read = read();
builder.check()?;
read.map_err(Error::Invalid)?;
Ok(builder.finish(Side {
input: Input::Source,
profile: profile.id.clone(),
profile_version: profile.version.clone(),
source_digest: hex(&world.source_digest()),
}))
}
pub fn from_pack(pack: &Pack, limits: &Limits) -> Result<Self, Error> {
let mut builder = Builder::new(*limits);
let read = pack.visit_identities(&mut |identity| {
match identity {
Identity::RenderLayer { name, position } => builder.render_layer(name, position),
Identity::Plane { id, document } => builder.plane(id, document),
Identity::Zone {
plane,
id,
document,
} => builder.zone(plane, id, document),
Identity::Anchor {
plane,
id,
layer,
at,
} => builder.anchor(plane, id, layer, at),
Identity::Stairwell {
plane,
id,
document,
} => builder.stairwell(plane, id, document),
Identity::Definition { kind, id, document } => {
builder.definition(kind, id, document)
}
Identity::Chunk {
plane,
layer,
cx,
cy,
} => builder.chunk(plane, layer, cx, cy),
Identity::Object { uid, plane, .. } => builder.object(uid, plane),
}
.map_err(tilewright_pack::Error::Invalid)
});
builder.check()?;
read.map_err(Error::Pack)?;
let meta = |key: &str| -> Result<String, Error> {
pack.meta(key).map_err(Error::Pack)?.ok_or_else(|| {
Error::Pack(tilewright_pack::Error::Invalid(format!(
"the pack has no {key}"
)))
})
};
Ok(builder.finish(Side {
input: Input::Pack,
profile: meta("profile_id")?,
profile_version: meta("profile_version")?,
source_digest: meta("source_digest")?,
}))
}
#[must_use]
pub const fn side(&self) -> &Side {
&self.side
}
#[must_use]
pub fn contains(&self, kind: Kind, id: &str) -> bool {
self.ids.get(&kind).is_some_and(|ids| ids.contains_key(id))
}
#[must_use]
pub fn object_plane(&self, uid: &str) -> Option<&str> {
self.object_planes.get(uid).map(String::as_str)
}
pub fn ids(&self, kind: Kind) -> impl Iterator<Item = &str> {
self.ids
.get(&kind)
.into_iter()
.flat_map(|ids| ids.keys().map(String::as_str))
}
pub(crate) fn entries(&self, kind: Kind) -> impl Iterator<Item = (&str, &Fingerprint)> {
self.ids.get(&kind).into_iter().flat_map(|ids| {
ids.iter()
.map(|(id, fingerprint)| (id.as_str(), fingerprint))
})
}
}
struct Builder {
ids: BTreeMap<Kind, BTreeMap<String, Fingerprint>>,
planes: BTreeMap<String, (Fingerprint, Sha256)>,
object_planes: BTreeMap<String, String>,
maximum: usize,
count: usize,
}
impl Builder {
const fn new(limits: Limits) -> Self {
Self {
ids: BTreeMap::new(),
planes: BTreeMap::new(),
object_planes: BTreeMap::new(),
maximum: limits.maximum_ids,
count: 0,
}
}
fn admit(&mut self) -> Result<(), String> {
self.count = self.count.saturating_add(1);
if self.count > self.maximum {
return Err(format!("more than {} IDs", self.maximum));
}
Ok(())
}
const fn check(&self) -> Result<(), Error> {
if self.count > self.maximum {
Err(Error::TooManyIds(self.maximum))
} else {
Ok(())
}
}
fn insert(&mut self, kind: Kind, id: String, fingerprint: Fingerprint) -> Result<(), String> {
let ids = self.ids.entry(kind).or_default();
if ids.contains_key(&id) {
return Err(format!("{kind} {id} is listed twice"));
}
ids.insert(id, fingerprint);
Ok(())
}
fn render_layer(&mut self, name: &str, position: usize) -> Result<(), String> {
self.admit()?;
if !is_identifier(name) {
return Err("a render layer is not an identifier".into());
}
let fingerprint = Sha256::digest(position.to_string()).into();
self.insert(Kind::RenderLayer, name.to_owned(), fingerprint)
}
fn plane(&mut self, id: &str, document: &str) -> Result<(), String> {
self.admit()?;
let fingerprint = document_fingerprint(document, &["id", "title"])?;
if self
.planes
.insert(id.to_owned(), (fingerprint, Sha256::new()))
.is_some()
{
return Err(format!("plane {id} is listed twice"));
}
Ok(())
}
fn zone(&mut self, plane: &str, id: &str, document: &str) -> Result<(), String> {
self.admit()?;
let fingerprint = document_fingerprint(document, &["id", "name"])?;
self.insert(Kind::Zone, format!("{plane}/{id}"), fingerprint)
}
fn anchor(&mut self, plane: &str, id: &str, layer: i32, at: [i64; 2]) -> Result<(), String> {
self.admit()?;
let content = format!("{{\"layer\":{layer},\"at\":[{},{}]}}", at[0], at[1]);
self.insert(
Kind::Anchor,
format!("{plane}/{id}"),
Sha256::digest(content).into(),
)
}
fn stairwell(&mut self, plane: &str, id: &str, document: &str) -> Result<(), String> {
self.admit()?;
let fingerprint = document_fingerprint(document, &["id"])?;
self.insert(Kind::Stairwell, format!("{plane}/{id}"), fingerprint)
}
fn definition(&mut self, kind: &str, id: &str, document: &str) -> Result<(), String> {
self.admit()?;
let fingerprint = document_fingerprint(document, &["id"])?;
self.insert(Kind::Definition, format!("{kind}/{id}"), fingerprint)
}
fn object(&mut self, uid: &str, plane: &str) -> Result<(), String> {
self.admit()?;
if !is_identifier(uid) || !is_identifier(plane) {
return Err("an object uid or its plane is not an identifier".into());
}
self.insert(Kind::Object, uid.to_owned(), [0; 32])?;
self.object_planes.insert(uid.to_owned(), plane.to_owned());
Ok(())
}
fn chunk(&mut self, plane: &str, layer: i32, cx: i64, cy: i64) -> Result<(), String> {
let (_, chunks) = self
.planes
.get_mut(plane)
.ok_or_else(|| format!("a chunk names unknown plane {plane}"))?;
chunks.update(layer.to_le_bytes());
chunks.update(cx.to_le_bytes());
chunks.update(cy.to_le_bytes());
Ok(())
}
fn finish(self, side: Side) -> Snapshot {
let Self {
mut ids,
planes,
object_planes,
..
} = self;
let mut fingerprints = BTreeMap::new();
for (plane, (document, chunks)) in planes {
let mut hasher = Sha256::new();
hasher.update(document);
hasher.update(chunks.finalize());
let prefix = format!("{plane}/");
for kind in [Kind::Anchor, Kind::Stairwell, Kind::Zone] {
let Some(children) = ids.get(&kind) else {
continue;
};
let range = children
.range::<str, _>((Bound::Included(prefix.as_str()), Bound::Unbounded))
.take_while(|(id, _)| id.starts_with(&prefix));
for (id, fingerprint) in range {
feed(&mut hasher, kind.as_str().as_bytes());
feed(&mut hasher, &id.as_bytes()[prefix.len()..]);
hasher.update(fingerprint);
}
}
fingerprints.insert(plane, hasher.finalize().into());
}
ids.insert(Kind::Plane, fingerprints);
Snapshot {
side,
ids,
object_planes,
}
}
}
fn feed(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
hasher.update(bytes);
}
fn canonical_limits() -> ParseLimits {
let limits = ParseLimits::default();
ParseLimits {
max_line_bytes: limits.max_input_bytes,
..limits
}
}
fn document_fingerprint(document: &str, without: &[&str]) -> Result<Fingerprint, String> {
let parsed = Document::parse(document, &canonical_limits())
.map_err(|error| format!("a stored document cannot be read again: {error}"))?;
let members = parsed
.root()
.as_object()
.ok_or("a stored document is not an object")?;
let excluded: BTreeSet<&str> = without.iter().copied().collect();
let mut hasher = Sha256::new();
hasher.update(b"{");
let mut first = true;
let mut key = String::new();
for member in members
.iter()
.filter(|member| !excluded.contains(member.key.as_str()))
{
if !first {
hasher.update(b",");
}
first = false;
key.clear();
write_json_string(&member.key, &mut key);
hasher.update(key.as_bytes());
hasher.update(b":");
hasher.update(parsed.canonical(&member.value).as_bytes());
}
hasher.update(b"}");
Ok(hasher.finalize().into())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().fold(String::new(), |mut output, byte| {
let _ = write!(output, "{byte:02x}");
output
})
}