newt_interaction/downgrade.rs
1//! Reading a record this build may not have been written for, and
2//! presenting one a surface may not be able to satisfy.
3//!
4//! Two ADR laws meet here. Law 1: stripping metadata always leaves a
5//! useful document, so an unrecognized record is kept VERBATIM rather than
6//! normalized into whatever this build happens to understand. Law 5:
7//! unknown REQUIRED behavior fails closed, unknown OPTIONAL behavior
8//! degrades visibly.
9//!
10//! The rule that keeps both honest is that a forward version is never
11//! PARTIALLY interpreted. Reading the fields we recognize out of a v2
12//! record and calling the result a v1 is the tempting failure — it yields
13//! a record that looks valid and carries an id its author never minted.
14
15use content_addressable::{canonical, ContentError};
16use serde::de::DeserializeOwned;
17use serde::{Deserialize, Serialize};
18
19use crate::definition::{FeatureDemand, InteractionDefinition, Requirement, SurfaceFeature};
20use crate::error::ProtocolError;
21use crate::instance::InteractionInstance;
22use crate::response::Response;
23
24/// Why a record was preserved instead of interpreted.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum UnknownReason {
28 /// The tag names a LATER version of the record type that was asked
29 /// for. The operator's fix is a newer build.
30 ForwardVersion,
31 /// The tag names a DIFFERENT record type in this protocol — an
32 /// instance handed to the definition decoder, say. The fix is a
33 /// routing correction, not an upgrade, and saying "forward version"
34 /// would send someone looking for a release that will never help.
35 WrongRecordType,
36 /// The tag is not one of this protocol's at all: foreign data, or
37 /// corruption that happened to leave a readable string.
38 UnknownSchema,
39 /// The tag is ours, but the record does not fit the shape this build
40 /// knows — most often a field we have no name for. Its requiredness is
41 /// unknowable, so it is never interpreted.
42 Uninterpretable,
43}
44
45/// A record this build did not interpret, kept whole.
46///
47/// The bytes are the payload: not re-serialized, not normalized, not
48/// trimmed. A consumer can pass them on, store them, or show them, and
49/// whoever does understand the version gets exactly what was written.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct RawRecord {
52 schema: String,
53 bytes: Vec<u8>,
54 reason: UnknownReason,
55}
56
57impl RawRecord {
58 /// The schema tag the record declared.
59 #[must_use]
60 pub fn schema(&self) -> &str {
61 &self.schema
62 }
63
64 /// The original bytes, verbatim.
65 #[must_use]
66 pub fn bytes(&self) -> &[u8] {
67 &self.bytes
68 }
69
70 /// Why it was not interpreted.
71 #[must_use]
72 pub fn reason(&self) -> UnknownReason {
73 self.reason
74 }
75}
76
77/// The result of reading a record: understood, or preserved.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum Decoded<T> {
80 /// The tag was known, the shape fit, and the bytes were canonical.
81 Known(T),
82 /// Anything else. Deliberately no partial interpretation.
83 Unknown(RawRecord),
84}
85
86/// Just enough of any record to read its tag without committing to a shape.
87///
88/// **Deliberately NOT `deny_unknown_fields`, and that absence is
89/// load-bearing.** Every other `Deserialize` type here forbids unknown
90/// fields; this one must tolerate them, because its entire job is to read
91/// the tag out of a record whose remaining shape is exactly what we do not
92/// yet know. Adding the attribute for consistency would make every real
93/// record fail the probe and turn all three gates into `Malformed`,
94/// including records this build understands perfectly.
95/// `the_probe_reads_a_tag_it_does_not_understand` pins it.
96#[derive(Deserialize)]
97struct SchemaProbe {
98 schema: String,
99}
100
101/// Read a record of type `T`, preserving it whole unless this build can
102/// account for every byte.
103///
104/// Three gates, in order, and each exists because the one before it is not
105/// enough on its own:
106///
107/// 1. **The tag.** A version we do not have is preserved (law 1) and never
108/// guessed at.
109/// 2. **The shape.** `deny_unknown_fields` refuses a record carrying a
110/// field we have no name for. Serde's default is to DROP such a field,
111/// which silently discards whatever it said — possibly a required
112/// demand — and yields a record whose id its author never minted.
113/// 3. **The bytes.** Even a record that decodes may not be the record that
114/// was written: reordered map keys, a non-minimal integer, an
115/// indefinite-length string all decode fine and re-encode differently.
116///
117/// Gates 2 and 3 are now ONE call. `content-addressable` 0.1.2 added
118/// `from_canonical_dagcbor_checked`, which decodes and then verifies that
119/// re-encoding the decoded value reproduces the input — the check this
120/// module used to perform by hand, delegated to the crate that owns
121/// canonical encoding. It still does not rest on decoder strictness: the
122/// verification is a FORWARD encoding, which is the rule this repo learned
123/// the hard way (decoder strictness drifts between releases).
124fn decode<T>(bytes: &[u8], expected: &'static str) -> Result<Decoded<T>, ProtocolError>
125where
126 T: DeserializeOwned + Serialize,
127{
128 // The probe reads a SUBSET of the record on purpose — the tag, and
129 // nothing else — so the checked door's guarantee (re-encoding the
130 // decoded value reproduces the input) is not merely unmet here but
131 // meaningless: a tag-only struct cannot re-encode to a whole record.
132 // The crate sanctions exactly this caller with a local
133 // `#[allow(deprecated)]` ("callers who want the value, not the codec
134 // check"). Every decode that produces a RECORD goes through the
135 // checked door below; this one only decides which door to open.
136 #[allow(deprecated)]
137 let probe: Result<SchemaProbe, _> = canonical::from_canonical_dagcbor(bytes);
138 let probe = probe.map_err(|e| ProtocolError::Malformed {
139 reason: format!("no readable schema tag: {e}"),
140 })?;
141 if probe.schema != expected {
142 let reason = classify_tag(&probe.schema, expected);
143 return Ok(Decoded::Unknown(RawRecord {
144 schema: probe.schema,
145 bytes: bytes.to_vec(),
146 reason,
147 }));
148 }
149
150 // Gates 2 and 3, now the crate's. `from_canonical_dagcbor_checked`
151 // decodes AND verifies that re-encoding the decoded value reproduces
152 // the input bytes — the check this module used to perform by hand.
153 // Its three verdicts map onto ours:
154 match canonical::from_canonical_dagcbor_checked::<T>(bytes) {
155 Ok(record) => Ok(Decoded::Known(record)),
156 // The bytes decode but are not the canonical encoding of what they
157 // decode to. Accepting them would mint an id different from the
158 // one their author published.
159 Err(ContentError::NonCanonical) => Err(ProtocolError::NonCanonical {
160 schema: probe.schema,
161 input_len: bytes.len(),
162 }),
163 // The typed round trip lost something — an unknown field, a
164 // narrowing conversion. The probe already proved these bytes are a
165 // record carrying OUR tag, so this is "shaped like a record we
166 // cannot fully account for", not "not a record", and it is
167 // preserved rather than partially read.
168 //
169 // A post-tag DecodingError lands here too, deliberately. Once the
170 // tag matched, a serde refusal (`deny_unknown_fields` firing, say)
171 // is the same fact as a lossy round trip: readable record, our
172 // version, this build cannot account for every byte. Reporting it
173 // as `Malformed` would conflate that with bytes that are not a
174 // record at all, which is the distinction the probe stage exists
175 // to draw.
176 Err(ContentError::LossyDecode | ContentError::DecodingError { .. }) => {
177 Ok(Decoded::Unknown(RawRecord {
178 schema: probe.schema,
179 bytes: bytes.to_vec(),
180 reason: UnknownReason::Uninterpretable,
181 }))
182 }
183 Err(other) => Err(ProtocolError::Identity(other)),
184 }
185}
186
187/// Which kind of schema mismatch this is.
188///
189/// Tags are `newt.interaction.<record>/v<n>`, so the record name is the
190/// segment before the last `/`. Same record, different version is a
191/// version gap; a different record in our namespace is a routing mistake;
192/// anything else is not ours.
193fn classify_tag(seen: &str, expected: &'static str) -> UnknownReason {
194 let family = |tag: &str| tag.rsplit_once('/').map(|(head, _)| head.to_string());
195 match (family(seen), family(expected)) {
196 (Some(seen_family), Some(expected_family)) if seen_family == expected_family => {
197 UnknownReason::ForwardVersion
198 }
199 (Some(seen_family), _) if seen_family.starts_with("newt.interaction.") => {
200 UnknownReason::WrongRecordType
201 }
202 _ => UnknownReason::UnknownSchema,
203 }
204}
205
206/// Read a definition. See [`decode`] for the three gates.
207///
208/// # Errors
209///
210/// [`ProtocolError::Malformed`] when the bytes are not a readable record;
211/// [`ProtocolError::NonCanonical`] when they decode but are not the
212/// canonical encoding of what they decode to.
213pub fn decode_definition(bytes: &[u8]) -> Result<Decoded<InteractionDefinition>, ProtocolError> {
214 decode(bytes, crate::definition::DEFINITION_SCHEMA_V1)
215}
216
217/// Read an instance. See [`decode`] for the three gates.
218///
219/// # Errors
220///
221/// As [`decode_definition`].
222pub fn decode_instance(bytes: &[u8]) -> Result<Decoded<InteractionInstance>, ProtocolError> {
223 decode(bytes, crate::instance::INSTANCE_SCHEMA_V1)
224}
225
226/// Read a response. See [`decode`] for the three gates.
227///
228/// # Errors
229///
230/// As [`decode_definition`].
231pub fn decode_response(bytes: &[u8]) -> Result<Decoded<Response>, ProtocolError> {
232 decode(bytes, crate::response::RESPONSE_SCHEMA_V1)
233}
234
235/// One demand a surface could not meet, reported rather than swallowed.
236///
237/// A consumer-side RESULT, not a wire record: it is computed here from a
238/// definition and a surface's capabilities, and nothing ever parses one.
239/// It therefore derives no serde traits at all — which is why it carries
240/// no `deny_unknown_fields`, and why it is absent from the schema and
241/// vector corpora. A2 publishes the records that travel; how A3 chooses to
242/// report a shortfall outward is A3's envelope to define.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct Degradation {
245 feature: SurfaceFeature,
246}
247
248impl Degradation {
249 /// The feature that went unmet.
250 #[must_use]
251 pub fn feature(&self) -> &str {
252 self.feature.as_str()
253 }
254}
255
256/// What a surface can honestly present, and what it had to give up.
257///
258/// A consumer-side result like [`Degradation`], and not on the wire for
259/// the same reason.
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct Presentation {
262 degradations: Vec<Degradation>,
263}
264
265impl Presentation {
266 /// Every optional demand this surface could not meet, in the order the
267 /// definition asked for them.
268 #[must_use]
269 pub fn degradations(&self) -> &[Degradation] {
270 &self.degradations
271 }
272
273 /// Whether every demand was met.
274 ///
275 /// Named for what a caller wants to know. A surface that dropped a
276 /// diagram is still usable, but it is not showing what was written,
277 /// and the difference should be visible rather than inferred from an
278 /// empty list.
279 #[must_use]
280 pub fn is_faithful(&self) -> bool {
281 self.degradations.is_empty()
282 }
283}
284
285/// Capabilities a definition's own controls REQUIRE, whether or not its
286/// author restated them in `features`.
287///
288/// A Secret control needs a surface that accepts secret input without
289/// echoing or persisting it — that is what makes it a secret control, not
290/// a separate claim its author might forget. Reading only the declared
291/// `features` let a definition with a required Secret control report
292/// `is_faithful() == true` on a surface that cannot handle one, which is
293/// the shortfall that matters most.
294///
295/// Derived rather than validated at construction on purpose: derivation
296/// has NO wire impact, so it stays correctable if the rule turns out
297/// wrong. Requiring authors to declare it — or refusing records that do
298/// not — would change which records are representable, and this slice
299/// freezes that.
300///
301/// The control's own [`Requirement`] carries through: an optional secret
302/// field degrades visibly rather than refusing, or the derivation would be
303/// stricter than its author asked for.
304fn intrinsic_demands(definition: &InteractionDefinition) -> Vec<FeatureDemand> {
305 let mut demands: Vec<FeatureDemand> = Vec::new();
306 for control in &definition.controls {
307 let Some(feature) = (match control.kind {
308 crate::definition::ControlKind::Secret => Some(SurfaceFeature::SECRET_INPUT),
309 _ => None,
310 }) else {
311 continue;
312 };
313 let Ok(feature) = SurfaceFeature::new(feature) else {
314 continue;
315 };
316 // A feature demanded by two controls is one demand; the stricter
317 // requirement wins, since satisfying it satisfies both.
318 match demands.iter_mut().find(|d| d.feature == feature) {
319 Some(existing) => {
320 if control.requirement == Requirement::Required {
321 existing.requirement = Requirement::Required;
322 }
323 }
324 None => demands.push(FeatureDemand {
325 feature,
326 requirement: control.requirement,
327 }),
328 }
329 }
330 demands
331}
332
333/// Decide what `supported` can present of `definition`.
334///
335/// # Errors
336///
337/// [`ProtocolError::UnsupportedFeature`] when a REQUIRED demand cannot be
338/// met — whether because this build does not recognize the feature or
339/// because the surface lacks it. Both are the same fact to the operator:
340/// this document cannot be shown faithfully, and guessing is not on offer.
341pub fn plan_presentation(
342 definition: &InteractionDefinition,
343 supported: &[SurfaceFeature],
344) -> Result<Presentation, ProtocolError> {
345 let mut degradations = Vec::new();
346 for demand in intrinsic_demands(definition)
347 .iter()
348 .chain(definition.features.iter())
349 {
350 if supported.contains(&demand.feature) {
351 continue;
352 }
353 match demand.requirement {
354 Requirement::Required => {
355 return Err(ProtocolError::UnsupportedFeature {
356 feature: demand.feature.as_str().to_string(),
357 known: demand.feature.is_known(),
358 })
359 }
360 Requirement::Optional => degradations.push(Degradation {
361 feature: demand.feature.clone(),
362 }),
363 }
364 }
365 Ok(Presentation { degradations })
366}