1use 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#[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 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 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
83static OWNER: std::sync::OnceLock<Result<String, String>> = std::sync::OnceLock::new();
85
86pub fn local_owner() -> Result<Caller, Failure> {
97 let name = OWNER
98 .get_or_init(login_name)
99 .as_ref()
100 .map_err(|why| Failure::new("local_owner_unknown", why.clone()))?;
101 Caller::local(name.clone())
102}
103
104#[cfg(test)]
107pub(crate) fn test_author() -> &'static Caller {
108 static AUTHOR: std::sync::OnceLock<Caller> = std::sync::OnceLock::new();
109 AUTHOR.get_or_init(|| Caller {
110 issuer: "local".to_owned(),
111 subject: "standalone".to_owned(),
112 responsible: None,
113 })
114}
115
116fn login_name() -> Result<String, String> {
120 let output = std::process::Command::new("/usr/bin/id")
121 .arg("-un")
122 .stdin(std::process::Stdio::null())
123 .stderr(std::process::Stdio::null())
124 .output()
125 .map_err(|error| format!("/usr/bin/id could not be run: {error}"))?;
126 if !output.status.success() {
127 return Err(format!("/usr/bin/id -un answered {}", output.status));
128 }
129 let text = String::from_utf8(output.stdout)
130 .map_err(|_| "/usr/bin/id -un answered a name that is not UTF-8".to_owned())?;
131 let name = text.strip_suffix('\n').unwrap_or(&text);
132 if name.is_empty() || name.contains('\n') {
133 return Err("/usr/bin/id -un answered no single login name".to_owned());
134 }
135 Ok(name.to_owned())
136}
137
138impl TryFrom<CallerWire> for Caller {
139 type Error = Failure;
140
141 fn try_from(wire: CallerWire) -> Result<Self, Self::Error> {
142 Self::new(wire.issuer, wire.subject, wire.responsible)
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
149pub enum ReadDependency {
150 Document {
151 collection: String,
152 document_id: String,
153 },
154 Collection {
155 collection: String,
156 },
157 SourceInterval {
160 collection: String,
161 lower: Option<String>,
162 upper: Option<String>,
163 },
164}
165
166impl ReadDependency {
167 fn validate(&self) -> Result<(), Failure> {
168 match self {
169 Self::Document {
170 collection,
171 document_id,
172 } => {
173 identifier(collection)?;
174 identifier(document_id)
175 }
176 Self::Collection { collection } => identifier(collection),
177 Self::SourceInterval {
178 collection,
179 lower,
180 upper,
181 } => {
182 identifier(collection)?;
183 for bound in [lower, upper].into_iter().flatten() {
184 if bound.len() > 512 || bound.contains('\0') {
185 return Err(Failure::new(
186 "invalid_dependency",
187 "interval bounds must contain at most 512 UTF-8 bytes and no NUL",
188 ));
189 }
190 }
191 if matches!((lower, upper), (Some(lower), Some(upper)) if lower > upper) {
192 return Err(Failure::new(
193 "invalid_dependency",
194 "interval lower bound exceeds its exclusive upper bound",
195 ));
196 }
197 Ok(())
198 }
199 }
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
206pub enum Contract {
207 ExactVersion {
208 expected: Version,
209 },
210 Dependencies {
211 origin: Version,
212 reads: Vec<ReadDependency>,
213 },
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "snake_case")]
218pub enum ContractKind {
219 ExactVersion,
220 Dependencies,
221}
222
223impl Contract {
224 #[must_use]
225 pub const fn origin(&self) -> Version {
226 match self {
227 Self::ExactVersion { expected } => *expected,
228 Self::Dependencies { origin, .. } => *origin,
229 }
230 }
231
232 #[must_use]
233 pub const fn kind(&self) -> ContractKind {
234 match self {
235 Self::ExactVersion { .. } => ContractKind::ExactVersion,
236 Self::Dependencies { .. } => ContractKind::Dependencies,
237 }
238 }
239
240 fn validate(&self) -> Result<(), Failure> {
241 if let Self::Dependencies { reads, .. } = self {
242 for read in reads {
243 read.validate()?;
244 }
245 }
246 Ok(())
247 }
248}
249
250#[derive(Debug, Serialize, Deserialize)]
252#[serde(try_from = "MutationWire")]
253pub struct MutationRequest {
254 request_id: String,
255 branch: String,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 caller: Option<Caller>,
258 contract: Contract,
259 change: Change,
260}
261
262#[derive(Deserialize)]
263#[serde(deny_unknown_fields)]
264struct MutationWire {
265 request_id: String,
266 branch: String,
267 #[serde(default, deserialize_with = "present_caller")]
268 caller: Option<Caller>,
269 contract: Contract,
270 change: Change,
271}
272
273fn present_caller<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Caller>, D::Error> {
274 Caller::deserialize(deserializer).map(Some)
275}
276
277impl TryFrom<MutationWire> for MutationRequest {
278 type Error = Failure;
279
280 fn try_from(wire: MutationWire) -> Result<Self, Self::Error> {
281 identifier(&wire.request_id)?;
282 identifier(&wire.branch)?;
283 wire.contract.validate()?;
284 if let Change::Fork(fork) = &wire.change {
285 identifier(&fork.name)?;
286 placeable(&fork.name)?;
287 }
288 Ok(Self {
289 request_id: wire.request_id,
290 branch: wire.branch,
291 caller: wire.caller,
292 contract: wire.contract,
293 change: wire.change,
294 })
295 }
296}
297
298impl MutationRequest {
299 pub fn decode(bytes: &[u8]) -> Result<Self, Failure> {
304 serde_json::from_slice(bytes)
305 .map_err(|error| Failure::new("invalid_mutation", error.to_string()))
306 }
307
308 pub fn resolve(self, socket_owner_subject: &str) -> Result<ResolvedMutation, Failure> {
313 let caller = match self.caller {
314 Some(caller) => caller,
315 None => Caller::local(socket_owner_subject)?,
316 };
317 Ok(ResolvedMutation {
318 request_id: self.request_id,
319 branch: self.branch,
320 caller,
321 contract: self.contract,
322 change: self.change,
323 })
324 }
325}
326
327#[derive(Debug, Serialize)]
329pub struct ResolvedMutation {
330 request_id: String,
331 branch: String,
332 caller: Caller,
333 contract: Contract,
334 change: Change,
335}
336
337impl ResolvedMutation {
338 #[must_use]
339 pub fn request_id(&self) -> &str {
340 &self.request_id
341 }
342
343 #[must_use]
344 pub fn branch(&self) -> &str {
345 &self.branch
346 }
347
348 #[must_use]
349 pub const fn caller(&self) -> &Caller {
350 &self.caller
351 }
352
353 #[must_use]
354 pub const fn contract(&self) -> &Contract {
355 &self.contract
356 }
357
358 #[must_use]
359 pub const fn change(&self) -> &Change {
360 &self.change
361 }
362
363 #[must_use]
364 pub const fn origin(&self) -> Version {
365 self.contract.origin()
366 }
367
368 pub fn fingerprint(&self) -> Result<String, Failure> {
373 let mut bytes = FINGERPRINT_DOMAIN.to_vec();
374 bytes.extend_from_slice(&FINGERPRINT_FORMAT.to_be_bytes());
375 serde_json::to_writer(&mut bytes, self).map_err(Failure::storage)?;
376 Ok(Hash::of(&bytes).to_string())
377 }
378
379 pub(super) fn destination(&self) -> &str {
380 match &self.change {
381 Change::Fork(fork) => &fork.name,
382 _ => &self.branch,
383 }
384 }
385}
386
387pub(super) fn placeable(name: &str) -> Result<(), Failure> {
392 if name
393 .bytes()
394 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
395 {
396 return Ok(());
397 }
398 Err(Failure::new(
399 "branch_name_unplaceable",
400 "a new branch name may hold only ASCII letters, digits, `_`, `-` and `.`, so Lys can place it",
401 ))
402}
403
404pub(super) fn identifier(value: &str) -> Result<(), Failure> {
405 if value.is_empty() || value.len() > 512 || value.contains('\0') {
406 return Err(Failure::new(
407 "invalid_identifier",
408 "names must contain 1 to 512 UTF-8 bytes and no NUL",
409 ));
410 }
411 Ok(())
412}
413
414#[cfg(test)]
415#[path = "contracts/placeable_tests.rs"]
416mod placeable_tests;