1use loro::{LoroDoc, LoroMap, LoroValue};
15
16use crate::error::{ArrayError, ArrayResult};
17use crate::schema::array_schema::ArraySchema;
18use crate::sync::hlc::{Hlc, HlcGenerator};
19use crate::sync::replica_id::ReplicaId;
20
21pub const LORO_FORMAT_VERSION: u8 = 1;
31
32pub const MAX_LORO_SCHEMA_SNAPSHOT_BYTES: usize = 16 * 1024 * 1024;
35pub const MAX_LORO_SCHEMA_SNAPSHOT_OPERATIONS: usize = 100_000;
37
38#[derive(Debug, Clone, Copy)]
39struct SchemaSnapshotLimits {
40 max_bytes: usize,
41 max_operations: usize,
42}
43
44impl Default for SchemaSnapshotLimits {
45 fn default() -> Self {
46 Self {
47 max_bytes: MAX_LORO_SCHEMA_SNAPSHOT_BYTES,
48 max_operations: MAX_LORO_SCHEMA_SNAPSHOT_OPERATIONS,
49 }
50 }
51}
52
53pub struct SchemaDoc {
62 doc: LoroDoc,
63 schema_hlc: Hlc,
64 replica_id: ReplicaId,
65}
66
67impl SchemaDoc {
68 pub fn new(replica_id: ReplicaId) -> Self {
73 Self {
74 doc: LoroDoc::new(),
75 schema_hlc: Hlc::ZERO,
76 replica_id,
77 }
78 }
79
80 pub fn from_schema(
86 replica_id: ReplicaId,
87 schema: &ArraySchema,
88 generator: &HlcGenerator,
89 ) -> ArrayResult<Self> {
90 let mut doc_self = Self::new(replica_id);
91 doc_self.write_schema_to_doc(schema)?;
92 doc_self.schema_hlc = generator.next()?;
93 Ok(doc_self)
94 }
95
96 pub fn schema_hlc(&self) -> Hlc {
98 self.schema_hlc
99 }
100
101 pub fn replica_id(&self) -> ReplicaId {
103 self.replica_id
104 }
105
106 pub fn to_schema(&self) -> ArrayResult<ArraySchema> {
111 let root = self.doc.get_map("root");
112 let bytes = self.read_content_bytes(&root)?;
113 zerompk::from_msgpack(&bytes).map_err(|e| ArrayError::SegmentCorruption {
114 detail: format!("schema decode failed: {e}"),
115 })
116 }
117
118 pub fn export_snapshot(&self) -> ArrayResult<Vec<u8>> {
127 let loro_bytes = self.doc.export(loro::ExportMode::Snapshot).map_err(|e| {
128 ArrayError::SegmentCorruption {
129 detail: format!("loro snapshot export failed: {e}"),
130 }
131 })?;
132 let mut envelope = Vec::with_capacity(1 + loro_bytes.len());
133 envelope.push(LORO_FORMAT_VERSION);
134 envelope.extend_from_slice(&loro_bytes);
135 Ok(envelope)
136 }
137
138 pub fn import_snapshot(
146 &mut self,
147 bytes: &[u8],
148 remote_hlc: Hlc,
149 generator: &HlcGenerator,
150 ) -> ArrayResult<()> {
151 self.import_snapshot_with_limits(
152 bytes,
153 remote_hlc,
154 generator,
155 SchemaSnapshotLimits::default(),
156 )
157 }
158
159 fn import_snapshot_with_limits(
160 &mut self,
161 bytes: &[u8],
162 remote_hlc: Hlc,
163 generator: &HlcGenerator,
164 limits: SchemaSnapshotLimits,
165 ) -> ArrayResult<()> {
166 let loro_bytes = admit_snapshot(bytes, limits)?;
167 self.doc
168 .import(loro_bytes)
169 .map_err(|e| ArrayError::LoroError {
170 detail: format!("loro import failed: {e}"),
171 })?;
172 generator.observe(remote_hlc)?;
173 self.schema_hlc = generator.next()?;
174 Ok(())
175 }
176
177 pub fn import_snapshot_replicated(
188 &mut self,
189 bytes: &[u8],
190 committed_hlc: Hlc,
191 ) -> ArrayResult<()> {
192 self.import_snapshot_replicated_with_limits(
193 bytes,
194 committed_hlc,
195 SchemaSnapshotLimits::default(),
196 )
197 }
198
199 fn import_snapshot_replicated_with_limits(
200 &mut self,
201 bytes: &[u8],
202 committed_hlc: Hlc,
203 limits: SchemaSnapshotLimits,
204 ) -> ArrayResult<()> {
205 let loro_bytes = admit_snapshot(bytes, limits)?;
206 self.doc
207 .import(loro_bytes)
208 .map_err(|e| ArrayError::LoroError {
209 detail: format!("loro import (replicated) failed: {e}"),
210 })?;
211 self.schema_hlc = committed_hlc;
212 Ok(())
213 }
214
215 pub fn replace_schema(
223 &mut self,
224 schema: &ArraySchema,
225 generator: &HlcGenerator,
226 ) -> ArrayResult<()> {
227 self.write_schema_to_doc(schema)?;
228 self.schema_hlc = generator.next()?;
229 Ok(())
230 }
231
232 fn write_schema_to_doc(&self, schema: &ArraySchema) -> ArrayResult<()> {
235 let schema_bytes =
236 zerompk::to_msgpack_vec(schema).map_err(|e| ArrayError::SegmentCorruption {
237 detail: format!("schema encode failed: {e}"),
238 })?;
239 let root: LoroMap = self.doc.get_map("root");
240 root.insert("content", LoroValue::Binary(schema_bytes.into()))
241 .map_err(|e| ArrayError::LoroError {
242 detail: format!("loro map insert failed: {e}"),
243 })?;
244 Ok(())
245 }
246
247 fn read_content_bytes(&self, root: &LoroMap) -> ArrayResult<Vec<u8>> {
248 match root.get("content") {
249 Some(loro::ValueOrContainer::Value(LoroValue::Binary(b))) => Ok(b.to_vec()),
250 Some(other) => Err(ArrayError::SegmentCorruption {
251 detail: format!("expected Binary at root[\"content\"], got {:?}", other),
252 }),
253 None => Err(ArrayError::SegmentCorruption {
254 detail: "root[\"content\"] not found".into(),
255 }),
256 }
257 }
258}
259
260fn admit_snapshot(bytes: &[u8], limits: SchemaSnapshotLimits) -> ArrayResult<&[u8]> {
268 if bytes.len() > limits.max_bytes {
269 return Err(ArrayError::LoroSnapshotTooLarge {
270 limit: limits.max_bytes,
271 actual: bytes.len(),
272 });
273 }
274 let loro_bytes = strip_envelope(bytes)?;
275 let metadata = loro::LoroDoc::decode_import_blob_meta(loro_bytes, true).map_err(|error| {
276 ArrayError::LoroSnapshotMalformed {
277 detail: error.to_string(),
278 }
279 })?;
280 let operation_count =
281 encoded_operation_count(&metadata.partial_start_vv, &metadata.partial_end_vv)?;
282 if operation_count > limits.max_operations {
283 return Err(ArrayError::LoroSnapshotOperationLimitExceeded {
284 limit: limits.max_operations,
285 actual: operation_count,
286 });
287 }
288 Ok(loro_bytes)
289}
290
291fn encoded_operation_count(
292 start: &loro::VersionVector,
293 end: &loro::VersionVector,
294) -> ArrayResult<usize> {
295 for (peer, start_counter) in start.iter() {
296 if end.get(peer).copied().unwrap_or_default() < *start_counter {
297 return Err(ArrayError::LoroSnapshotMalformed {
298 detail: "Loro operation range regresses".into(),
299 });
300 }
301 }
302 end.iter().try_fold(0usize, |total, (peer, end_counter)| {
303 let start_counter = start.get(peer).copied().unwrap_or_default();
304 let operations = end_counter.checked_sub(start_counter).ok_or_else(|| {
305 ArrayError::LoroSnapshotMalformed {
306 detail: "Loro operation range regresses".into(),
307 }
308 })?;
309 let operations =
310 usize::try_from(operations).map_err(|_| ArrayError::LoroSnapshotMalformed {
311 detail: "Loro operation count does not fit platform usize".into(),
312 })?;
313 total
314 .checked_add(operations)
315 .ok_or_else(|| ArrayError::LoroSnapshotMalformed {
316 detail: "Loro operation count overflow".into(),
317 })
318 })
319}
320
321fn strip_envelope(bytes: &[u8]) -> ArrayResult<&[u8]> {
322 match bytes.first() {
323 None => Err(ArrayError::SegmentCorruption {
324 detail: "loro snapshot envelope is empty".into(),
325 }),
326 Some(&v) if v != LORO_FORMAT_VERSION => Err(ArrayError::LoroSnapshotVersionMismatch {
327 expected: LORO_FORMAT_VERSION,
328 got: v,
329 }),
330 Some(_) => Ok(&bytes[1..]),
331 }
332}
333
334#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::schema::array_schema::ArraySchema;
340 use crate::schema::attr_spec::{AttrSpec, AttrType};
341 use crate::schema::cell_order::{CellOrder, TileOrder};
342 use crate::schema::dim_spec::{DimSpec, DimType};
343 use crate::sync::hlc::HlcGenerator;
344 use crate::sync::replica_id::ReplicaId;
345 use crate::types::domain::{Domain, DomainBound};
346
347 fn replica(id: u64) -> ReplicaId {
348 ReplicaId::new(id)
349 }
350
351 fn generator(id: u64) -> HlcGenerator {
352 HlcGenerator::new(replica(id))
353 }
354
355 fn simple_schema(name: &str) -> ArraySchema {
356 ArraySchema {
357 name: name.into(),
358 dims: vec![DimSpec::new(
359 "x",
360 DimType::Int64,
361 Domain::new(DomainBound::Int64(0), DomainBound::Int64(99)),
362 )],
363 attrs: vec![AttrSpec::new("v", AttrType::Float64, true)],
364 tile_extents: vec![10],
365 cell_order: CellOrder::RowMajor,
366 tile_order: TileOrder::RowMajor,
367 }
368 }
369
370 #[test]
371 fn from_schema_then_to_schema_roundtrips() {
372 let g = generator(1);
373 let schema = simple_schema("arr");
374 let doc = SchemaDoc::from_schema(replica(1), &schema, &g).unwrap();
375 let back = doc.to_schema().unwrap();
376 assert_eq!(schema, back);
377 assert!(doc.schema_hlc() > Hlc::ZERO);
378 }
379
380 #[test]
381 fn replace_schema_bumps_hlc() {
382 let g = generator(1);
383 let schema = simple_schema("arr");
384 let mut doc = SchemaDoc::from_schema(replica(1), &schema, &g).unwrap();
385 let hlc_before = doc.schema_hlc();
386
387 let schema2 = simple_schema("arr2");
388 doc.replace_schema(&schema2, &g).unwrap();
389 assert!(doc.schema_hlc() > hlc_before);
390 assert_eq!(doc.to_schema().unwrap(), schema2);
391 }
392
393 #[test]
394 fn export_then_import_converges() {
395 let g_a = generator(1);
396 let schema = simple_schema("shared");
397 let doc_a = SchemaDoc::from_schema(replica(1), &schema, &g_a).unwrap();
398 let snapshot = doc_a.export_snapshot().unwrap();
399
400 let g_b = generator(2);
401 let mut doc_b = SchemaDoc::new(replica(2));
402 doc_b
403 .import_snapshot(&snapshot, doc_a.schema_hlc(), &g_b)
404 .unwrap();
405
406 assert_eq!(doc_a.to_schema().unwrap(), doc_b.to_schema().unwrap());
407 }
408
409 #[test]
410 fn import_observes_remote_hlc() {
411 let g_a = generator(1);
412 let schema = simple_schema("x");
413 let doc_a = SchemaDoc::from_schema(replica(1), &schema, &g_a).unwrap();
414 let remote_hlc = doc_a.schema_hlc();
415 let snapshot = doc_a.export_snapshot().unwrap();
416
417 let g_b = generator(2);
418 let mut doc_b = SchemaDoc::new(replica(2));
419 doc_b.import_snapshot(&snapshot, remote_hlc, &g_b).unwrap();
420
421 doc_b.replace_schema(&simple_schema("x2"), &g_b).unwrap();
423 assert!(doc_b.schema_hlc() > remote_hlc);
424 }
425
426 #[test]
427 fn import_garbage_errors() {
428 let g = generator(1);
429 let mut doc = SchemaDoc::new(replica(1));
430 let mut bad = vec![LORO_FORMAT_VERSION];
433 bad.extend_from_slice(b"not valid loro data");
434 let result = doc.import_snapshot(&bad, Hlc::ZERO, &g);
435 assert!(result.is_err());
436 }
437
438 #[test]
439 fn export_snapshot_has_version_prefix() {
440 let g = generator(1);
441 let schema = simple_schema("arr");
442 let doc = SchemaDoc::from_schema(replica(1), &schema, &g).unwrap();
443 let snapshot = doc.export_snapshot().unwrap();
444 assert!(!snapshot.is_empty());
445 assert_eq!(snapshot[0], LORO_FORMAT_VERSION);
446 }
447
448 #[test]
449 fn pre_import_admission_rejections_preserve_schema_and_hlc() {
450 let generator = generator(1);
451 let baseline = simple_schema("baseline");
452 let mut doc = SchemaDoc::from_schema(replica(1), &baseline, &generator).unwrap();
453 let before_hlc = doc.schema_hlc();
454 let limits = SchemaSnapshotLimits {
455 max_bytes: 1,
456 max_operations: 1,
457 };
458 let result = doc.import_snapshot_with_limits(
459 &[LORO_FORMAT_VERSION, 0],
460 Hlc::ZERO,
461 &generator,
462 limits,
463 );
464 assert!(matches!(
465 result,
466 Err(ArrayError::LoroSnapshotTooLarge { .. })
467 ));
468 assert_eq!(doc.to_schema().unwrap(), baseline);
469 assert_eq!(doc.schema_hlc(), before_hlc);
470
471 let result = doc.import_snapshot_with_limits(
472 &[LORO_FORMAT_VERSION, 0xff],
473 Hlc::ZERO,
474 &generator,
475 SchemaSnapshotLimits::default(),
476 );
477 assert!(matches!(
478 result,
479 Err(ArrayError::LoroSnapshotMalformed { .. })
480 ));
481 assert_eq!(doc.to_schema().unwrap(), baseline);
482 assert_eq!(doc.schema_hlc(), before_hlc);
483 }
484
485 #[test]
486 fn operation_admission_rejects_before_mutating_replicated_schema() {
487 let source_generator = generator(2);
488 let source =
489 SchemaDoc::from_schema(replica(2), &simple_schema("source"), &source_generator)
490 .unwrap();
491 let snapshot = source.export_snapshot().unwrap();
492
493 let target_generator = generator(1);
494 let baseline = simple_schema("baseline");
495 let mut target = SchemaDoc::from_schema(replica(1), &baseline, &target_generator).unwrap();
496 let before_hlc = target.schema_hlc();
497 let result = target.import_snapshot_replicated_with_limits(
498 &snapshot,
499 source.schema_hlc(),
500 SchemaSnapshotLimits {
501 max_bytes: snapshot.len(),
502 max_operations: 0,
503 },
504 );
505 assert!(matches!(
506 result,
507 Err(ArrayError::LoroSnapshotOperationLimitExceeded { .. })
508 ));
509 assert_eq!(target.to_schema().unwrap(), baseline);
510 assert_eq!(target.schema_hlc(), before_hlc);
511 }
512
513 #[test]
514 fn import_snapshot_rejects_wrong_version() {
515 let g_a = generator(1);
516 let schema = simple_schema("v");
517 let doc_a = SchemaDoc::from_schema(replica(1), &schema, &g_a).unwrap();
518 let mut snapshot = doc_a.export_snapshot().unwrap();
519
520 snapshot[0] = LORO_FORMAT_VERSION.wrapping_add(1);
522
523 let g_b = generator(2);
524 let mut doc_b = SchemaDoc::new(replica(2));
525 let err = doc_b
526 .import_snapshot(&snapshot, doc_a.schema_hlc(), &g_b)
527 .unwrap_err();
528 assert!(
529 matches!(
530 err,
531 ArrayError::LoroSnapshotVersionMismatch { expected, got }
532 if expected == LORO_FORMAT_VERSION && got == LORO_FORMAT_VERSION.wrapping_add(1)
533 ),
534 "unexpected error: {err}"
535 );
536 }
537
538 #[test]
539 fn import_snapshot_replicated_rejects_wrong_version() {
540 let g_a = generator(1);
541 let schema = simple_schema("v2");
542 let doc_a = SchemaDoc::from_schema(replica(1), &schema, &g_a).unwrap();
543 let mut snapshot = doc_a.export_snapshot().unwrap();
544
545 snapshot[0] = 0; let mut doc_b = SchemaDoc::new(replica(2));
548 let err = doc_b
549 .import_snapshot_replicated(&snapshot, doc_a.schema_hlc())
550 .unwrap_err();
551 assert!(
552 matches!(
553 err,
554 ArrayError::LoroSnapshotVersionMismatch { expected, got }
555 if expected == LORO_FORMAT_VERSION && got == 0
556 ),
557 "unexpected error: {err}"
558 );
559 }
560}