use std::collections::BTreeMap;
use std::num::NonZeroUsize;
use cadmpeg_core::decode::{u64_from_index, DecodeContext};
use cadmpeg_core::CodecError;
#[cfg(feature = "schema")]
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
pub mod bytes;
mod canon;
pub mod catalogue;
mod compare;
mod copy;
mod replay;
pub(crate) mod view;
#[cfg(test)]
pub(crate) fn test_ctx() -> DecodeContext<'static> {
let arena = Box::leak(Box::new(cadmpeg_core::decode::DecodeArena::new()));
DecodeContext::from_root_bytes(&[], arena, &cadmpeg_core::decode::DecodePolicy::service())
.expect("empty test input fits the service policy")
.0
}
const MAX_NATIVE_NESTING_DEPTH: usize = 256;
fn nests_too_deep_message() -> String {
format!("native value nests deeper than {MAX_NATIVE_NESTING_DEPTH} containers")
}
fn nests_past(value: &Value, limit: usize) -> bool {
let mut pending: Vec<(&Value, usize)> = Vec::new();
push_container(&mut pending, value, 1);
while let Some((value, depth)) = pending.pop() {
if depth > limit {
return true;
}
match value {
Value::Array(items) => {
for item in items {
push_container(&mut pending, item, depth + 1);
}
}
Value::Object(entries) => {
for entry in entries.values() {
push_container(&mut pending, entry, depth + 1);
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
false
}
fn push_container<'a>(pending: &mut Vec<(&'a Value, usize)>, value: &'a Value, depth: usize) {
if matches!(value, Value::Array(_) | Value::Object(_)) {
pending.push((value, depth));
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LossCount {
pub format: String,
pub kind: String,
pub count: NonZeroUsize,
}
#[derive(Debug, thiserror::Error)]
pub enum NativeConvertError {
#[error(transparent)]
Resource(#[from] cadmpeg_core::CodecError),
#[error("native record is missing a string id")]
MissingId,
#[error("native record has an invalid identity: {0}")]
InvalidIdentity(#[from] crate::ids::IdentityError),
#[error("native record did not serialize as an object")]
NonObject,
#[error(
"native record {id}: field {field} nests deeper than {} containers",
MAX_NATIVE_NESTING_DEPTH
)]
FieldNestsTooDeep {
id: crate::ids::Identity,
field: String,
},
#[error("native record field {field} holds a non-finite number")]
NonFiniteNumber {
field: String,
},
#[error("native record conversion failed: {0}")]
Serde(#[from] serde_json::Error),
#[error("native record conversion failed: {0}")]
ConversionMessage(String),
#[error("native record {id}: {source}")]
ReadRecord {
id: crate::ids::Identity,
#[source]
source: serde_json::Error,
},
#[error("native input record at ordinal {ordinal}: {source}")]
WriteRecord {
ordinal: usize,
#[source]
source: Box<NativeConvertError>,
},
#[error("native arena {arena}: {source}")]
Arena {
arena: String,
#[source]
source: Box<NativeConvertError>,
},
#[error("native record has an invalid owner: {0}")]
InvalidOwner(String),
#[error("native collection is invalid: {0}")]
InvalidCollection(String),
#[error("native unknown record has no retained source record: {0}")]
MissingRetainedSourceRecord(String),
}
impl From<NativeConvertError> for cadmpeg_core::CodecError {
fn from(error: NativeConvertError) -> Self {
if let Some(limit) = error.resource_limit() {
Self::ResourceLimit(limit)
} else {
Self::Malformed(error.to_string())
}
}
}
impl NativeConvertError {
fn resource_limit(&self) -> Option<cadmpeg_core::decode::ResourceLimit> {
match self {
Self::Resource(cadmpeg_core::CodecError::ResourceLimit(limit)) => Some(*limit),
Self::WriteRecord { source, .. } | Self::Arena { source, .. } => {
source.resource_limit()
}
_ => None,
}
}
}
#[derive(Serialize)]
#[cfg(feature = "schema")]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename = "NativeRecord")]
struct RecordShape<'a> {
id: crate::ids::Identity,
#[serde(flatten)]
fields: &'a Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NativeField {
Text(String),
TextList(Vec<String>),
}
impl NativeField {
fn into_value(self) -> Value {
match self {
Self::Text(text) => Value::String(text),
Self::TextList(items) => Value::Array(items.into_iter().map(Value::String).collect()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct NativeRecord {
id: crate::ids::Identity,
json: Box<str>,
}
impl NativeRecord {
pub fn new(
id: crate::ids::Identity,
mut fields: Map<String, Value>,
) -> Result<Self, NativeConvertError> {
fields.remove("id");
for (field, value) in &fields {
if nests_past(value, MAX_NATIVE_NESTING_DEPTH) {
return Err(NativeConvertError::FieldNestsTooDeep {
id,
field: field.clone(),
});
}
}
let json = render(&id, &fields);
Ok(Self { id, json })
}
pub fn from_identity(
id: impl Into<crate::ids::Identity>,
fields: impl IntoIterator<Item = (String, NativeField)>,
) -> Self {
let id = id.into();
let mut stored = Map::new();
for (name, value) in fields {
if name != "id" {
stored.insert(name, value.into_value());
}
}
let json = render(&id, &stored);
Self { id, json }
}
#[cfg(test)]
pub(crate) fn from_typed<T: Serialize>(record: &T) -> Result<Self, NativeConvertError> {
let arena = cadmpeg_core::decode::DecodeArena::new();
let mut policy = cadmpeg_core::decode::DecodePolicy::default();
policy.limits.max_recursion_depth = u64_from_index(MAX_NATIVE_NESTING_DEPTH + 1);
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy)?;
Self::from_typed_for_decode(&ctx, record)
}
fn from_typed_for_decode<T: Serialize>(
ctx: &DecodeContext<'_>,
record: &T,
) -> Result<Self, NativeConvertError> {
let record = Self::canonical(ctx, record)?;
ctx.retain_bytes(u64_from_index(record.json.len()), "serialize native record")?;
Ok(record)
}
fn canonical<T: Serialize>(
ctx: &DecodeContext<'_>,
record: &T,
) -> Result<Self, NativeConvertError> {
let mut out = Vec::new();
let serialized = record
.serialize(canon::CanonText::for_record(ctx, &mut out))
.map_err(|error| error.into_native(ctx))?;
let canon::Written::Object(members) = serialized else {
return Err(NativeConvertError::NonObject);
};
let (id, json) = members.into_record(out)?;
Ok(Self { id, json })
}
pub(crate) fn digest_unknown(&self, ctx: &DecodeContext<'_>) -> Result<Self, CodecError> {
let fields = self.fields()?;
for key in fields.keys() {
if key != "links" {
return Err(CodecError::malformed(format_args!(
"native unknown {} has unexpected field {key}",
self.id
)));
}
}
let links = match fields.get("links") {
None => &[][..],
Some(Value::Array(links)) => links.as_slice(),
Some(_) => {
return Err(CodecError::malformed(format_args!(
"native unknown {} links is not a sequence",
self.id
)))
}
};
for value in links {
let Value::String(link) = value else {
return Err(CodecError::malformed(format_args!(
"native unknown {} link is not text",
self.id
)));
};
if !crate::ids::is_valid_identity(link) {
return Err(CodecError::malformed(format_args!(
"native unknown {} has invalid link {link}",
self.id
)));
}
}
Self::canonical(
ctx,
&DigestUnknown {
id: self.id.as_str(),
links,
},
)
.map_err(CodecError::from)
}
#[must_use]
pub fn id(&self) -> &str {
self.id.as_str()
}
pub(crate) fn text_len(&self) -> usize {
self.json.len()
}
pub fn fields(&self) -> Result<Map<String, Value>, NativeConvertError> {
let value = replay::value(&self.json).map_err(|source| NativeConvertError::ReadRecord {
id: self.id.clone(),
source,
})?;
let Value::Object(mut fields) = value else {
return Err(NativeConvertError::NonObject);
};
fields.remove("id");
Ok(fields)
}
pub fn copy_fields(
&self,
ctx: &DecodeContext<'_>,
) -> Result<Map<String, Value>, NativeConvertError> {
Ok(copy::fields(ctx, &self.fields()?)?)
}
pub fn rewrite_fields<T, F>(
&self,
ctx: &DecodeContext<'_>,
map: F,
) -> Result<Map<String, Value>, NativeConvertError>
where
T: crate::schema::rewrite::typed::RewriteIdentities,
F: FnMut(&str) -> Result<String, cadmpeg_core::CodecError>,
{
let mut fields = self.fields()?;
fields.insert("id".to_owned(), Value::String(self.id.as_str().to_owned()));
let mut value = Value::Object(fields);
let mut identities = crate::schema::rewrite::typed::IdentityMap::new(map);
T::rewrite_native_value(ctx, &mut value, &mut identities)?;
identities.finish()?;
let Value::Object(mut fields) = value else {
return Err(
cadmpeg_core::CodecError::malformed("native record must be an object").into(),
);
};
fields.remove("id");
Ok(fields)
}
#[must_use]
pub fn field(&self, name: &str) -> Option<Value> {
if name == "id" {
return None;
}
replay::value(replay::member(&self.json, name)?.get()).ok()
}
fn to_typed<T: DeserializeOwned>(
&self,
ctx: &DecodeContext<'_>,
) -> Result<T, NativeConvertError> {
let mut frames = Vec::new();
for _ in 0..container_depth(&self.json) {
frames.push(ctx.enter_nested(canon::WORK)?);
}
let mut deserializer = serde_json::Deserializer::from_str(&self.json);
deserializer.disable_recursion_limit();
T::deserialize(&mut deserializer)
.and_then(|value| deserializer.end().map(|()| value))
.map_err(|source| NativeConvertError::ReadRecord {
id: self.id.clone(),
source: without_position(source),
})
}
pub fn matches_typed_for_decode<T: Serialize + ?Sized>(
&self,
ctx: &DecodeContext<'_>,
expected: &T,
) -> Result<bool, NativeConvertError> {
compare::matches(ctx, self, expected)
}
}
#[derive(Serialize)]
struct DigestUnknown<'a> {
id: &'a str,
#[serde(skip_serializing_if = "<[Value]>::is_empty")]
links: &'a [Value],
}
fn container_depth(json: &str) -> usize {
let mut depth = 0_usize;
let mut deepest = 0;
let mut in_string = false;
let mut escaped = false;
for byte in json.bytes() {
if in_string {
if escaped {
escaped = false;
} else if byte == b'\\' {
escaped = true;
} else if byte == b'"' {
in_string = false;
}
continue;
}
match byte {
b'"' => in_string = true,
b'[' | b'{' => {
depth += 1;
deepest = deepest.max(depth);
}
b']' | b'}' => depth -= 1,
_ => {}
}
}
deepest
}
fn without_position(error: serde_json::Error) -> serde_json::Error {
if error.line() == 0 {
return error;
}
let suffix = format!(" at line {} column {}", error.line(), error.column());
let message = error.to_string();
match message.strip_suffix(&suffix) {
Some(message) => <serde_json::Error as serde::de::Error>::custom(message),
None => error,
}
}
fn read_record<T: DeserializeOwned>(
ctx: &DecodeContext<'_>,
arena: &str,
record: &NativeRecord,
) -> Result<T, NativeConvertError> {
match record.to_typed(ctx) {
Ok(value) => Ok(value),
Err(source) if source.resource_limit().is_some() => Err(source),
Err(source) => {
let arena = arena.to_owned();
Err(NativeConvertError::Arena {
arena,
source: Box::new(source),
})
}
}
}
impl Serialize for NativeRecord {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
replay::transcode(&self.json, serializer)
}
}
fn render(id: &crate::ids::Identity, fields: &Map<String, Value>) -> Box<str> {
let mut json = String::from("{\"id\":");
json.push_str(&Value::String(id.as_str().to_owned()).to_string());
for (key, value) in fields {
json.push(',');
json.push_str(&Value::String(key.clone()).to_string());
json.push(':');
json.push_str(&value.to_string());
}
json.push('}');
json.into_boxed_str()
}
impl<'de> Deserialize<'de> for NativeRecord {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let mut fields = cadmpeg_core::distinct_keys::json_object(deserializer)?;
let Some(Value::String(id)) = fields.remove("id") else {
return Err(<D::Error as serde::de::Error>::custom(
NativeConvertError::MissingId,
));
};
Self::new(
crate::ids::Identity::new(id).map_err(serde::de::Error::custom)?,
fields,
)
.map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "schema")]
impl JsonSchema for NativeRecord {
fn schema_name() -> std::borrow::Cow<'static, str> {
"NativeRecord".into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
concat!(module_path!(), "::NativeRecord").into()
}
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
RecordShape::json_schema(generator)
}
}
pub fn arena_from<T, E, I>(ctx: &DecodeContext<'_>, records: I) -> Result<Vec<NativeRecord>, E>
where
T: Serialize,
E: From<NativeConvertError>,
I: IntoIterator<Item = Result<T, E>>,
{
let mut converted = Vec::new();
for (ordinal, record) in records.into_iter().enumerate() {
let record = record?;
let record = match NativeRecord::from_typed_for_decode(ctx, &record) {
Ok(record) => record,
Err(source) if source.resource_limit().is_some() => return Err(E::from(source)),
Err(source) => {
return Err(E::from(NativeConvertError::WriteRecord {
ordinal,
source: Box::new(source),
}));
}
};
converted.push(record);
}
converted.sort_by(|left, right| left.id().cmp(right.id()));
Ok(converted)
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(transparent)]
pub struct NativeNamespace {
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
arenas: BTreeMap<String, Vec<NativeRecord>>,
}
impl NativeNamespace {
pub fn admit<'a, T>(&'a self) -> Result<T, NativeConvertError>
where
T: TryFrom<&'a Self, Error = NativeConvertError>,
{
T::try_from(self)
}
#[must_use]
pub fn arenas(&self) -> &BTreeMap<String, Vec<NativeRecord>> {
&self.arenas
}
pub fn arenas_mut(&mut self) -> &mut BTreeMap<String, Vec<NativeRecord>> {
&mut self.arenas
}
pub fn set_arena<T: Serialize>(
&mut self,
ctx: &DecodeContext<'_>,
name: impl AsRef<str>,
records: &[T],
) -> Result<(), NativeConvertError> {
self.set_arena_from(ctx, name, records.iter())
}
pub fn set_arena_from<T: Serialize, I: IntoIterator<Item = T>>(
&mut self,
ctx: &DecodeContext<'_>,
name: impl AsRef<str>,
records: I,
) -> Result<(), NativeConvertError> {
let name = name.as_ref().to_owned();
let converted = match arena_from(ctx, records.into_iter().map(Ok::<T, NativeConvertError>))
{
Ok(converted) => converted,
Err(source) if source.resource_limit().is_some() => return Err(source),
Err(source) => {
return Err(NativeConvertError::Arena {
arena: name,
source: Box::new(source),
});
}
};
self.arenas.insert(name, converted);
Ok(())
}
pub fn arena_as_collection<T, C>(&self, name: &str) -> Result<C, NativeConvertError>
where
T: DeserializeOwned,
C: TryFrom<Vec<T>, Error = NativeConvertError>,
{
let arena = cadmpeg_core::decode::DecodeArena::new();
let policy = cadmpeg_core::decode::DecodePolicy::default();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy)?;
self.arena_as_collection_for_decode(&ctx, name)
}
pub fn arena_as_collection_for_decode<T, C>(
&self,
ctx: &DecodeContext<'_>,
name: &str,
) -> Result<C, NativeConvertError>
where
T: DeserializeOwned,
C: TryFrom<Vec<T>, Error = NativeConvertError>,
{
C::try_from(self.arena_as_for_decode(ctx, name)?)
}
pub fn arena_as<T: DeserializeOwned>(&self, name: &str) -> Result<Vec<T>, NativeConvertError> {
let arena = cadmpeg_core::decode::DecodeArena::new();
let policy = cadmpeg_core::decode::DecodePolicy::default();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy)?;
self.arena_as_for_decode(&ctx, name)
}
pub fn arena_as_for_decode<T: DeserializeOwned>(
&self,
ctx: &DecodeContext<'_>,
name: &str,
) -> Result<Vec<T>, NativeConvertError> {
let mut typed = Vec::new();
if let Some((arena, records)) = self.arenas.get_key_value(name) {
for record in records {
typed.push(read_record(ctx, arena, record)?);
}
}
Ok(typed)
}
pub fn arena_iter_as<'a, T: DeserializeOwned + 'a>(
&'a self,
name: &str,
) -> impl Iterator<Item = Result<T, NativeConvertError>> + 'a {
self.arenas
.get_key_value(name)
.into_iter()
.flat_map(|(name, records)| {
records.iter().map(move |record| {
let arena = cadmpeg_core::decode::DecodeArena::new();
let policy = cadmpeg_core::decode::DecodePolicy::default();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy)?;
read_record(&ctx, name, record)
})
})
}
pub fn arena_iter_as_for_decode<'a, T: DeserializeOwned + 'a>(
&'a self,
ctx: &'a DecodeContext<'_>,
name: &str,
) -> impl Iterator<Item = Result<T, NativeConvertError>> + 'a {
self.arenas
.get_key_value(name)
.into_iter()
.flat_map(move |(arena, records)| {
records
.iter()
.map(move |record| read_record(ctx, arena, record))
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(transparent)]
pub struct Native(
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
pub BTreeMap<String, NativeNamespace>,
);
impl Native {
pub fn namespace(&self, format: &str) -> Option<&NativeNamespace> {
self.0.get(format)
}
pub fn namespace_mut(&mut self, format: impl Into<String>) -> &mut NativeNamespace {
self.0.entry(format.into()).or_default()
}
pub(crate) fn finalize(&mut self) {
for namespace in self.0.values_mut() {
for records in namespace.arenas.values_mut() {
records.sort_by(|left, right| left.id().cmp(right.id()));
}
}
}
pub fn loss_counts(&self) -> Vec<LossCount> {
self.0
.iter()
.flat_map(|(format, namespace)| {
namespace.arenas.iter().filter_map(move |(kind, records)| {
let count = NonZeroUsize::new(records.len())?;
Some(LossCount {
format: format.clone(),
kind: kind.clone(),
count,
})
})
})
.collect()
}
}
#[cfg(test)]
mod tests;