1use std::panic::{AssertUnwindSafe, catch_unwind};
13
14use rustev_contract::bounded::{BoundError, ParseError};
15use rustev_contract::canonical::tagged_digest;
16use rustev_contract::replay::MAX_RESOLVED_BYTES;
17use rustev_contract::retention::{Availability, Retention, RetentionItem};
18use rustev_contract::time::Timestamp;
19use rustev_contract::{Document, DocumentError, schema};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Resolution {
24 Bytes(Vec<u8>),
26 Missing,
27 Erased,
29 Inaccessible,
31}
32
33pub trait Resolver {
37 fn resolve(&self, reference: &str, max_bytes: usize) -> Resolution;
38}
39
40pub struct NoExternal;
42
43impl Resolver for NoExternal {
44 fn resolve(&self, _reference: &str, _max_bytes: usize) -> Resolution {
45 Resolution::Inaccessible
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ItemFailure {
52 Unavailable(Availability),
53 Oversized,
56}
57
58#[derive(Debug)]
60pub struct Budget {
61 remaining: usize,
62}
63
64impl Default for Budget {
65 fn default() -> Self {
66 Budget {
67 remaining: MAX_RESOLVED_BYTES,
68 }
69 }
70}
71
72impl Budget {
73 pub fn remaining(&self) -> usize {
74 self.remaining
75 }
76}
77
78pub fn resolve_item<T>(
80 item: &RetentionItem,
81 now: Timestamp,
82 resolver: &dyn Resolver,
83 budget: &mut Budget,
84) -> Result<T, ItemFailure>
85where
86 T: Document + PartialEq,
87{
88 let bytes = item_bytes(item, now, resolver, budget)?;
89 check_bytes::<T>(item, &bytes)
90}
91
92pub fn item_bytes(
94 item: &RetentionItem,
95 now: Timestamp,
96 resolver: &dyn Resolver,
97 budget: &mut Budget,
98) -> Result<Vec<u8>, ItemFailure> {
99 let unavailable = |a| Err(ItemFailure::Unavailable(a));
100 if now >= item.retention.expires_at() {
101 return unavailable(Availability::Expired);
102 }
103 let bytes = match &item.retention {
104 Retention::DigestOnly { .. } => return unavailable(Availability::Missing),
105 Retention::Retained { bytes, .. } => bytes.as_bytes().to_vec(),
106 Retention::External { reference, .. } => {
107 let max = budget.remaining;
108 match catch_unwind(AssertUnwindSafe(|| resolver.resolve(reference, max))) {
109 Ok(Resolution::Bytes(b)) => b,
110 Ok(Resolution::Missing) => return unavailable(Availability::Missing),
111 Ok(Resolution::Erased) => return unavailable(Availability::Erased),
112 Ok(Resolution::Inaccessible) | Err(_) => {
113 return unavailable(Availability::Inaccessible);
114 }
115 }
116 }
117 };
118 if bytes.len() > budget.remaining {
120 return Err(ItemFailure::Oversized);
121 }
122 budget.remaining -= bytes.len();
123 Ok(bytes)
124}
125
126pub fn check_bytes<T>(item: &RetentionItem, bytes: &[u8]) -> Result<T, ItemFailure>
128where
129 T: Document + PartialEq,
130{
131 let fail = |a| Err(ItemFailure::Unavailable(a));
132 if tagged_digest(&item.document_schema, bytes) != item.retention.digest().as_str() {
133 return fail(Availability::Corrupt);
134 }
135 let version_3 = matches!(
136 (T::SCHEMA, item.document_schema.as_str()),
137 (schema::PLAN, schema::PLAN_V3)
138 | (schema::RUN, schema::RUN_V3)
139 | (schema::SNAPSHOT, schema::SNAPSHOT_V2)
140 );
141 if item.document_schema != T::SCHEMA && !version_3 {
142 return fail(Availability::Mismatched);
143 }
144 let doc = match T::parse(bytes) {
145 Ok(d) => d,
146 Err(DocumentError::Parse(ParseError::Bound(b))) => {
147 return match b {
148 BoundError::Syntax { .. }
149 | BoundError::InvalidUtf8 { .. }
150 | BoundError::TrailingData { .. }
151 | BoundError::Empty => fail(Availability::Corrupt),
152 BoundError::TooLarge { .. }
153 | BoundError::TooDeep { .. }
154 | BoundError::StringTooLong { .. }
155 | BoundError::CollectionTooLong { .. }
156 | BoundError::TooManyValues { .. }
157 | BoundError::NumberTooLong { .. } => Err(ItemFailure::Oversized),
158 BoundError::FractionalNumber { .. } | BoundError::DuplicateKey { .. } => {
159 fail(Availability::Mismatched)
160 }
161 };
162 }
163 Err(_) => return fail(Availability::Mismatched),
164 };
165 if matches!(T::SCHEMA, schema::PLAN | schema::RUN | schema::SNAPSHOT)
166 && doc.schema() != item.document_schema
167 {
168 return fail(Availability::Mismatched);
169 }
170 match doc.record_canonical() {
172 Ok(c) if c == bytes => {}
173 _ => return fail(Availability::Mismatched),
174 }
175 if item
176 .identity
177 .as_ref()
178 .is_some_and(|id| id != item.retention.digest())
179 {
180 return fail(Availability::Mismatched);
181 }
182 Ok(doc)
183}