1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
//! Replicated documents for the Offline Protocol SDK.
//!
//! Messaging is synced events; this crate is synced state. A document is a
//! set of collections that any member of a space can edit while offline,
//! merging deterministically when the replicas meet again.
//!
//! # What this crate deliberately does not do
//!
//! It does not persist, encrypt, or send anything. It turns edits into
//! opaque byte deltas and turns byte deltas back into state, and that is
//! all. Storage, sealing and delivery belong to the main `offline-protocol`
//! crate, which already carries ten releases of reliability work this layer
//! inherits for free.
//!
//! # The engine is an implementation detail
//!
//! A CRDT engine is embedded here and named nowhere in this crate's public
//! API. No engine type appears in a signature, in the FFI surface, or in any
//! binding. One leaked engine type and the engine could never be replaced
//! without a breaking release everywhere, which is exactly the lock-in this
//! layer exists to avoid.
/// Document, collections, deltas and encoding.
/// Error types.
/// Size caps and the compaction trigger.
/// Values stored in collections.
pub use ;
pub use ;
pub use ;
pub use DataValue;
/// Longest accepted name for a space, document or collection.
pub const MAX_NAME_LEN: usize = 128;
/// Longest accepted map key.
pub const MAX_KEY_LEN: usize = 256;
/// Number of hex characters in an attachment hash, a SHA-256.
pub const ATTACHMENT_HASH_LEN: usize = 64;
/// Longest accepted attachment display name.
///
/// Shorter than a file system's limit on purpose. This is a label shown
/// next to the thing, not a path anything opens, and it replicates to every
/// member of the space whether or not they ever fetch the blob.
pub const MAX_ATTACHMENT_NAME_LEN: usize = 256;
/// Longest accepted attachment media type.
///
/// RFC 6838 bounds a type and a subtype at 127 characters each, so this
/// accepts every registered name and leaves nothing to argue about.
pub const MAX_ATTACHMENT_MIME_LEN: usize = 255;
/// Largest size an attachment reference may declare.
///
/// Not a statement about how large a blob may be: what a transport will
/// carry is the transfer layer's business and is far smaller than this. It
/// is the largest value the engine can hold without changing meaning, since
/// the engine has one integer type and it is signed. A reference declaring
/// more is refused at the operation that writes it rather than clamped on
/// the way in, because a clamped size is a size its writer never wrote.
pub const MAX_ATTACHMENT_SIZE: u64 = i64MAX as u64;
/// Longest accepted single value, equal to the whole-document cap.
///
/// A value at or past this size can never fit inside a document that also
/// has to hold its own framing, so accepting one only defers the refusal to
/// a place where it is expensive: an unbounded value makes a commit whose
/// delta record cannot be written at all, and a delta that cannot be written
/// is re-exported larger at every retry. Refusing at the operation keeps that
/// failure in front of the caller, while the edit is still theirs to change.
pub const MAX_VALUE_BYTES: usize = MAX_DOC_BYTES;
/// Check a space, document, or collection name.
///
/// The charset is narrow on purpose. These names are composed into storage
/// record keys (`{space}/{doc}`), so a name containing a separator or a
/// path traversal would make a key that parses back into different parts
/// than it was built from. Rejecting the character is the cheap half of
/// that problem; the expensive half is discovering it after records exist.
/// Check a space name.
///
/// Looser than [`validate_name`] by exactly one character, the colon, and
/// the reason is that a space is named after the MLS scope it replicates
/// in. A 1:1 space is named by a peer address; a group space is named by
/// the group id, which is minted as `group:<uuid>`. Refusing the colon
/// would mean either giving group spaces a second, translated name — one
/// more mapping to keep consistent and to get wrong — or refusing group
/// replication outright.
///
/// The colon is safe here in a way it would not be in a document name.
/// Record keys are composed as `{space}/{doc}` and `{space}/{doc}/{seq}`
/// and are parsed by stripping the space prefix and splitting the
/// remainder on `/`, so only the separator itself can make a key parse
/// back into different parts than it was built from, and that stays
/// forbidden.
///
/// Document names deliberately keep the narrow charset: a peer names those
/// on the wire, and this one is never wire-supplied. A 1:1 space is derived
/// from the authenticated sender of the frame and a group space from the
/// group whose key decrypted it, so a peer cannot choose either.
/// Check a map key.
///
/// Keys are looser than names because they never enter a record key: they
/// live inside the document. The only requirements are non-empty and
/// bounded, so a single key cannot consume a meaningful share of the
/// document's byte budget on its own.
/// Check the size of a single value.
///
/// Bounds the one input the caller controls without limit. Note this is a
/// per-value bound only: the whole-document cap still applies, and is what
/// catches many small values adding up.
/// Check that a string is a well-formed attachment address.
///
/// The hash is checked for shape, not for reachability: whether anybody
/// still holds the bytes is not knowable here, and a reference to a blob
/// nobody has is a normal, recoverable state. What is not recoverable is a
/// hash that is not a hash, because no fetch can ever succeed and the
/// reference replicates to the whole space regardless.
///
/// Case matters. The hash is an address, and two spellings of one address
/// are two addresses: they would fetch twice, store twice, and compare
/// unequal while naming identical bytes. Lowercase is the canonical form,
/// and the uppercase spelling is refused rather than folded so that a
/// writer producing the wrong one hears about it.
///
/// Separate from [`validate_attachment`] because a hash also travels alone:
/// the frames that ask for a blob and refuse one carry nothing else. Checking
/// those through the whole-reference validator means inventing a size to
/// satisfy it, which ties a frame check to a bound that has nothing to do
/// with it and breaks the day that validator gains a cross-field rule.
/// Check an attachment reference.
///
/// Every bound this layer places on a reference, in one place, so that the
/// write path and the read path cannot disagree about what a valid reference
/// is. A reference that fails any of them reads as absent rather than as an
/// error on the read path: see [`crate::DataDoc`].
/// Check a value length against [`MAX_VALUE_BYTES`].
///
/// Split out for text insertions, which carry a `&str` rather than a
/// [`DataValue`].