Skip to main content

haematite_cli/serve/
contracts.rs

1//! Mutation identity and strict wire contracts for a single stored-format cut.
2
3use haematite::tree::Hash;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use super::protocol::{Change, Version};
7use super::storage::Failure;
8
9pub mod attribution;
10pub mod envelopes;
11
12mod receipt;
13pub use receipt::Receipt;
14
15pub(super) const FINGERPRINT_FORMAT: u32 = 1;
16const FINGERPRINT_DOMAIN: &[u8] = b"haem.mutation.fingerprint\0";
17
18/// An asserted identity, with no stored credential.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(try_from = "CallerWire")]
21pub struct Caller {
22    issuer: String,
23    subject: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    responsible: Option<String>,
26}
27
28#[derive(Deserialize)]
29#[serde(deny_unknown_fields)]
30struct CallerWire {
31    issuer: String,
32    subject: String,
33    responsible: Option<String>,
34}
35
36impl Caller {
37    /// Construct an asserted caller identity.
38    ///
39    /// # Errors
40    /// Returns `invalid_identifier` when the issuer, subject or responsible person has an invalid identifier.
41    pub fn new(
42        issuer: impl Into<String>,
43        subject: impl Into<String>,
44        responsible: Option<String>,
45    ) -> Result<Self, Failure> {
46        let caller = Self {
47            issuer: issuer.into(),
48            subject: subject.into(),
49            responsible,
50        };
51        identifier(&caller.issuer)?;
52        identifier(&caller.subject)?;
53        if let Some(person) = &caller.responsible {
54            identifier(person)?;
55        }
56        Ok(caller)
57    }
58
59    /// Construct the identity of a local socket owner.
60    ///
61    /// # Errors
62    /// Returns `invalid_identifier` for an invalid subject.
63    pub fn local(subject: impl Into<String>) -> Result<Self, Failure> {
64        Self::new("local", subject, None)
65    }
66
67    #[must_use]
68    pub fn issuer(&self) -> &str {
69        &self.issuer
70    }
71
72    #[must_use]
73    pub fn subject(&self) -> &str {
74        &self.subject
75    }
76
77    #[must_use]
78    pub fn responsible(&self) -> Option<&str> {
79        self.responsible.as_deref()
80    }
81}
82
83impl TryFrom<CallerWire> for Caller {
84    type Error = Failure;
85
86    fn try_from(wire: CallerWire) -> Result<Self, Self::Error> {
87        Self::new(wire.issuer, wire.subject, wire.responsible)
88    }
89}
90
91/// Facts read at the contract's origin, including absent rows and interval gaps.
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
94pub enum ReadDependency {
95    Document {
96        collection: String,
97        document_id: String,
98    },
99    Collection {
100        collection: String,
101    },
102    /// Document ids in a lower-inclusive, upper-exclusive interval; absent bounds
103    /// cover the collection boundary. Body and revision rows belong to this read.
104    SourceInterval {
105        collection: String,
106        lower: Option<String>,
107        upper: Option<String>,
108    },
109}
110
111impl ReadDependency {
112    fn validate(&self) -> Result<(), Failure> {
113        match self {
114            Self::Document {
115                collection,
116                document_id,
117            } => {
118                identifier(collection)?;
119                identifier(document_id)
120            }
121            Self::Collection { collection } => identifier(collection),
122            Self::SourceInterval {
123                collection,
124                lower,
125                upper,
126            } => {
127                identifier(collection)?;
128                for bound in [lower, upper].into_iter().flatten() {
129                    if bound.len() > 512 || bound.contains('\0') {
130                        return Err(Failure::new(
131                            "invalid_dependency",
132                            "interval bounds must contain at most 512 UTF-8 bytes and no NUL",
133                        ));
134                    }
135                }
136                if matches!((lower, upper), (Some(lower), Some(upper)) if lower > upper) {
137                    return Err(Failure::new(
138                        "invalid_dependency",
139                        "interval lower bound exceeds its exclusive upper bound",
140                    ));
141                }
142                Ok(())
143            }
144        }
145    }
146}
147
148/// Exact-version and read-dependency semantics remain distinct.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
151pub enum Contract {
152    ExactVersion {
153        expected: Version,
154    },
155    Dependencies {
156        origin: Version,
157        reads: Vec<ReadDependency>,
158    },
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub enum ContractKind {
164    ExactVersion,
165    Dependencies,
166}
167
168impl Contract {
169    #[must_use]
170    pub const fn origin(&self) -> Version {
171        match self {
172            Self::ExactVersion { expected } => *expected,
173            Self::Dependencies { origin, .. } => *origin,
174        }
175    }
176
177    #[must_use]
178    pub const fn kind(&self) -> ContractKind {
179        match self {
180            Self::ExactVersion { .. } => ContractKind::ExactVersion,
181            Self::Dependencies { .. } => ContractKind::Dependencies,
182        }
183    }
184
185    fn validate(&self) -> Result<(), Failure> {
186        if let Self::Dependencies { reads, .. } = self {
187            for read in reads {
188                read.validate()?;
189            }
190        }
191        Ok(())
192    }
193}
194
195/// An omitted caller is resolved by the trusted socket context, never by storage.
196#[derive(Debug, Serialize, Deserialize)]
197#[serde(try_from = "MutationWire")]
198pub struct MutationRequest {
199    request_id: String,
200    branch: String,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    caller: Option<Caller>,
203    contract: Contract,
204    change: Change,
205}
206
207#[derive(Deserialize)]
208#[serde(deny_unknown_fields)]
209struct MutationWire {
210    request_id: String,
211    branch: String,
212    #[serde(default, deserialize_with = "present_caller")]
213    caller: Option<Caller>,
214    contract: Contract,
215    change: Change,
216}
217
218fn present_caller<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Caller>, D::Error> {
219    Caller::deserialize(deserializer).map(Some)
220}
221
222impl TryFrom<MutationWire> for MutationRequest {
223    type Error = Failure;
224
225    fn try_from(wire: MutationWire) -> Result<Self, Self::Error> {
226        identifier(&wire.request_id)?;
227        identifier(&wire.branch)?;
228        wire.contract.validate()?;
229        if let Change::Fork(fork) = &wire.change {
230            identifier(&fork.name)?;
231            placeable(&fork.name)?;
232        }
233        Ok(Self {
234            request_id: wire.request_id,
235            branch: wire.branch,
236            caller: wire.caller,
237            contract: wire.contract,
238            change: wire.change,
239        })
240    }
241}
242
243impl MutationRequest {
244    /// Decode and validate a mutation request.
245    ///
246    /// # Errors
247    /// Returns `invalid_mutation` for malformed JSON, unknown members, invalid identifiers, invalid dependencies or an unplaceable fork name.
248    pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
249        serde_json::from_slice(bytes)
250            .map_err(|error| Failure::new("invalid_mutation", error.to_string()))
251    }
252
253    /// Resolve an omitted caller from the trusted socket owner.
254    ///
255    /// # Errors
256    /// Returns `invalid_identifier` if an omitted caller cannot be resolved from the supplied subject.
257    pub fn resolve(self, socket_owner_subject: &str) -> Result<ResolvedMutation, Failure> {
258        let caller = match self.caller {
259            Some(caller) => caller,
260            None => Caller::local(socket_owner_subject)?,
261        };
262        Ok(ResolvedMutation {
263            request_id: self.request_id,
264            branch: self.branch,
265            caller,
266            contract: self.contract,
267            change: self.change,
268        })
269    }
270}
271
272/// Only a request with a resolved caller can be fingerprinted or receipted.
273#[derive(Debug, Serialize)]
274pub struct ResolvedMutation {
275    request_id: String,
276    branch: String,
277    caller: Caller,
278    contract: Contract,
279    change: Change,
280}
281
282impl ResolvedMutation {
283    #[must_use]
284    pub fn request_id(&self) -> &str {
285        &self.request_id
286    }
287
288    #[must_use]
289    pub fn branch(&self) -> &str {
290        &self.branch
291    }
292
293    #[must_use]
294    pub const fn caller(&self) -> &Caller {
295        &self.caller
296    }
297
298    #[must_use]
299    pub const fn contract(&self) -> &Contract {
300        &self.contract
301    }
302
303    #[must_use]
304    pub const fn change(&self) -> &Change {
305        &self.change
306    }
307
308    #[must_use]
309    pub const fn origin(&self) -> Version {
310        self.contract.origin()
311    }
312
313    /// Hash the versioned canonical request identity.
314    ///
315    /// # Errors
316    /// Returns `storage_unavailable` if serializing the resolved request fails.
317    pub fn fingerprint(&self) -> Result<String, Failure> {
318        let mut bytes = FINGERPRINT_DOMAIN.to_vec();
319        bytes.extend_from_slice(&FINGERPRINT_FORMAT.to_be_bytes());
320        serde_json::to_writer(&mut bytes, self).map_err(Failure::storage)?;
321        Ok(Hash::of(&bytes).to_string())
322    }
323
324    pub(super) fn destination(&self) -> &str {
325        match &self.change {
326            Change::Fork(fork) => &fork.name,
327            _ => &self.branch,
328        }
329    }
330}
331
332/// A new branch is placed in Lys as `haematite.branch:<database>.<name>`, and a
333/// Lys resource id holds only ASCII letters, digits, `_`, `-` and `.`. A name
334/// outside that could be written but never placed, so no grant could ever
335/// reach it: it is refused at the fork, by name.
336pub(super) fn placeable(name: &str) -> Result<(), Failure> {
337    if name
338        .bytes()
339        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
340    {
341        return Ok(());
342    }
343    Err(Failure::new(
344        "branch_name_unplaceable",
345        "a new branch name may hold only ASCII letters, digits, `_`, `-` and `.`, so Lys can place it",
346    ))
347}
348
349pub(super) fn identifier(value: &str) -> Result<(), Failure> {
350    if value.is_empty() || value.len() > 512 || value.contains('\0') {
351        return Err(Failure::new(
352            "invalid_identifier",
353            "names must contain 1 to 512 UTF-8 bytes and no NUL",
354        ));
355    }
356    Ok(())
357}
358
359#[cfg(test)]
360#[path = "contracts/placeable_tests.rs"]
361mod placeable_tests;