use std::panic::{AssertUnwindSafe, catch_unwind};
use rustev_contract::bounded::{BoundError, ParseError};
use rustev_contract::canonical::tagged_digest;
use rustev_contract::replay::MAX_RESOLVED_BYTES;
use rustev_contract::retention::{Availability, Retention, RetentionItem};
use rustev_contract::time::Timestamp;
use rustev_contract::{Document, DocumentError, schema};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution {
Bytes(Vec<u8>),
Missing,
Erased,
Inaccessible,
}
pub trait Resolver {
fn resolve(&self, reference: &str, max_bytes: usize) -> Resolution;
}
pub struct NoExternal;
impl Resolver for NoExternal {
fn resolve(&self, _reference: &str, _max_bytes: usize) -> Resolution {
Resolution::Inaccessible
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemFailure {
Unavailable(Availability),
Oversized,
}
#[derive(Debug)]
pub struct Budget {
remaining: usize,
}
impl Default for Budget {
fn default() -> Self {
Budget {
remaining: MAX_RESOLVED_BYTES,
}
}
}
impl Budget {
pub fn remaining(&self) -> usize {
self.remaining
}
}
pub fn resolve_item<T>(
item: &RetentionItem,
now: Timestamp,
resolver: &dyn Resolver,
budget: &mut Budget,
) -> Result<T, ItemFailure>
where
T: Document + PartialEq,
{
let bytes = item_bytes(item, now, resolver, budget)?;
check_bytes::<T>(item, &bytes)
}
pub fn item_bytes(
item: &RetentionItem,
now: Timestamp,
resolver: &dyn Resolver,
budget: &mut Budget,
) -> Result<Vec<u8>, ItemFailure> {
let unavailable = |a| Err(ItemFailure::Unavailable(a));
if now >= item.retention.expires_at() {
return unavailable(Availability::Expired);
}
let bytes = match &item.retention {
Retention::DigestOnly { .. } => return unavailable(Availability::Missing),
Retention::Retained { bytes, .. } => bytes.as_bytes().to_vec(),
Retention::External { reference, .. } => {
let max = budget.remaining;
match catch_unwind(AssertUnwindSafe(|| resolver.resolve(reference, max))) {
Ok(Resolution::Bytes(b)) => b,
Ok(Resolution::Missing) => return unavailable(Availability::Missing),
Ok(Resolution::Erased) => return unavailable(Availability::Erased),
Ok(Resolution::Inaccessible) | Err(_) => {
return unavailable(Availability::Inaccessible);
}
}
}
};
if bytes.len() > budget.remaining {
return Err(ItemFailure::Oversized);
}
budget.remaining -= bytes.len();
Ok(bytes)
}
pub fn check_bytes<T>(item: &RetentionItem, bytes: &[u8]) -> Result<T, ItemFailure>
where
T: Document + PartialEq,
{
let fail = |a| Err(ItemFailure::Unavailable(a));
if tagged_digest(&item.document_schema, bytes) != item.retention.digest().as_str() {
return fail(Availability::Corrupt);
}
let version_3 = matches!(
(T::SCHEMA, item.document_schema.as_str()),
(schema::PLAN, schema::PLAN_V3)
| (schema::RUN, schema::RUN_V3)
| (schema::SNAPSHOT, schema::SNAPSHOT_V2)
);
if item.document_schema != T::SCHEMA && !version_3 {
return fail(Availability::Mismatched);
}
let doc = match T::parse(bytes) {
Ok(d) => d,
Err(DocumentError::Parse(ParseError::Bound(b))) => {
return match b {
BoundError::Syntax { .. }
| BoundError::InvalidUtf8 { .. }
| BoundError::TrailingData { .. }
| BoundError::Empty => fail(Availability::Corrupt),
BoundError::TooLarge { .. }
| BoundError::TooDeep { .. }
| BoundError::StringTooLong { .. }
| BoundError::CollectionTooLong { .. }
| BoundError::TooManyValues { .. }
| BoundError::NumberTooLong { .. } => Err(ItemFailure::Oversized),
BoundError::FractionalNumber { .. } | BoundError::DuplicateKey { .. } => {
fail(Availability::Mismatched)
}
};
}
Err(_) => return fail(Availability::Mismatched),
};
if matches!(T::SCHEMA, schema::PLAN | schema::RUN | schema::SNAPSHOT)
&& doc.schema() != item.document_schema
{
return fail(Availability::Mismatched);
}
match doc.record_canonical() {
Ok(c) if c == bytes => {}
_ => return fail(Availability::Mismatched),
}
if item
.identity
.as_ref()
.is_some_and(|id| id != item.retention.digest())
{
return fail(Availability::Mismatched);
}
Ok(doc)
}