Skip to main content

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}