use std::collections::{BTreeMap, BTreeSet};
use serde_json::{Map, Value, json};
use super::Shape;
pub(super) struct Entry {
pub(super) since: u32,
pub(super) signature: Vec<String>,
}
pub(super) struct Ledger {
pub(super) protocol: u32,
pub(super) shapes: BTreeMap<String, Entry>,
}
impl Ledger {
pub(super) fn read(text: &str) -> Self {
let value = serde_json::from_str::<Value>(text).unwrap_or(Value::Null);
let mut shapes = BTreeMap::new();
for (name, entry) in value
.get("shapes")
.and_then(Value::as_object)
.into_iter()
.flatten()
{
shapes.insert(
name.clone(),
Entry {
since: number(entry.get("since")),
signature: strings(entry.get("signature")),
},
);
}
Self {
protocol: number(value.get("protocol")),
shapes,
}
}
pub(super) fn render(&self) -> String {
let shapes: Map<String, Value> = self
.shapes
.iter()
.map(|(name, entry)| {
let body = json!({ "since": entry.since, "signature": entry.signature });
(name.clone(), body)
})
.collect();
let doc = json!({ "protocol": self.protocol, "shapes": shapes });
super::canonical(&doc)
}
}
fn number(value: Option<&Value>) -> u32 {
let raw = value.and_then(Value::as_u64).unwrap_or_default();
u32::try_from(raw).unwrap_or_default()
}
fn strings(value: Option<&Value>) -> Vec<String> {
value
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect()
})
.unwrap_or_default()
}
pub(super) fn signature(frames: &[Value]) -> Vec<String> {
let mut out = BTreeSet::new();
for frame in frames {
walk("", frame, &mut out);
}
out.into_iter().collect()
}
fn walk(path: &str, value: &Value, out: &mut BTreeSet<String>) {
out.insert(format!("{path}:{}", kind(value)));
match value {
Value::Object(map) => {
for (key, child) in map {
walk(&format!("{path}/{key}"), child, out);
}
}
Value::Array(items) => {
for item in items {
walk(&format!("{path}/[]"), item, out);
}
}
_ => {}
}
}
fn kind(value: &Value) -> &'static str {
match value {
Value::Object(_) => "object",
Value::Array(_) => "array",
Value::String(_) => "string",
Value::Number(_) => "number",
Value::Bool(_) => "bool",
Value::Null => "null",
}
}
pub(super) fn advance(
shapes: &[Shape],
previous: &Ledger,
protocol: u32,
) -> Result<Ledger, String> {
let fresh: BTreeMap<String, Vec<String>> = shapes
.iter()
.map(|shape| (shape.key(), signature(&shape.frames)))
.collect();
let moved: Vec<String> = previous
.shapes
.iter()
.filter(|(name, entry)| fresh.get(*name) != Some(&entry.signature))
.map(|(name, _)| name.clone())
.collect();
if !moved.is_empty() && protocol <= previous.protocol {
return Err(format!(
"these wire shapes changed at an unchanged protocol version: {}. \
A change to a shape already in use bumps the version: raise PROTOCOL \
in src/wire/hello.rs, then run `make corpus`.",
moved.join(", ")
));
}
let shapes = fresh
.into_iter()
.map(|(name, signature)| {
let held = previous
.shapes
.get(&name)
.filter(|e| e.signature == signature);
let since = held.map_or(protocol, |entry| entry.since);
(name, Entry { since, signature })
})
.collect();
Ok(Ledger { protocol, shapes })
}