1use bitstream::{BitReader, BitWriter};
4use schema::{schema_hash, ComponentDef, ComponentId, FieldCodec, FieldDef, FieldId};
5use wire::{decode_packet, encode_header, SectionTag, WirePacket};
6
7use crate::error::{CodecError, CodecResult, LimitKind, MaskKind, MaskReason, ValueReason};
8use crate::limits::CodecLimits;
9use crate::types::{EntityId, SnapshotTick};
10
11const VARINT_MAX_BYTES: usize = 5;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Snapshot {
16 pub tick: SnapshotTick,
17 pub entities: Vec<EntitySnapshot>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct EntitySnapshot {
23 pub id: EntityId,
24 pub components: Vec<ComponentSnapshot>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ComponentSnapshot {
30 pub id: ComponentId,
31 pub fields: Vec<FieldValue>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum FieldValue {
38 Bool(bool),
39 UInt(u64),
40 SInt(i64),
41 VarUInt(u64),
42 VarSInt(i64),
43 FixedPoint(i64),
44}
45
46pub fn encode_full_snapshot(
50 schema: &schema::Schema,
51 tick: SnapshotTick,
52 entities: &[EntitySnapshot],
53 limits: &CodecLimits,
54 out: &mut [u8],
55) -> CodecResult<usize> {
56 if out.len() < wire::HEADER_SIZE {
57 return Err(CodecError::OutputTooSmall {
58 needed: wire::HEADER_SIZE,
59 available: out.len(),
60 });
61 }
62
63 if entities.len() > limits.max_entities_create {
64 return Err(CodecError::LimitsExceeded {
65 kind: LimitKind::EntitiesCreate,
66 limit: limits.max_entities_create,
67 actual: entities.len(),
68 });
69 }
70
71 let mut offset = wire::HEADER_SIZE;
72 if !entities.is_empty() {
73 let written = write_section(
74 SectionTag::EntityCreate,
75 &mut out[offset..],
76 limits,
77 |writer| encode_create_body(schema, entities, limits, writer),
78 )?;
79 offset += written;
80 }
81
82 let payload_len = offset - wire::HEADER_SIZE;
83 let header =
84 wire::PacketHeader::full_snapshot(schema_hash(schema), tick.raw(), payload_len as u32);
85 encode_header(&header, &mut out[..wire::HEADER_SIZE]).map_err(|_| {
86 CodecError::OutputTooSmall {
87 needed: wire::HEADER_SIZE,
88 available: out.len(),
89 }
90 })?;
91
92 Ok(offset)
93}
94
95pub fn decode_full_snapshot(
97 schema: &schema::Schema,
98 bytes: &[u8],
99 wire_limits: &wire::Limits,
100 limits: &CodecLimits,
101) -> CodecResult<Snapshot> {
102 let packet = decode_packet(bytes, wire_limits)?;
103 decode_full_snapshot_from_packet(schema, &packet, limits)
104}
105
106pub fn decode_full_snapshot_from_packet(
108 schema: &schema::Schema,
109 packet: &WirePacket<'_>,
110 limits: &CodecLimits,
111) -> CodecResult<Snapshot> {
112 let header = packet.header;
113 if !header.flags.is_full_snapshot() {
114 return Err(CodecError::Wire(wire::DecodeError::InvalidFlags {
115 flags: header.flags.raw(),
116 }));
117 }
118 if header.baseline_tick != 0 {
119 return Err(CodecError::Wire(wire::DecodeError::InvalidBaselineTick {
120 baseline_tick: header.baseline_tick,
121 flags: header.flags.raw(),
122 }));
123 }
124
125 let expected_hash = schema_hash(schema);
126 if header.schema_hash != expected_hash {
127 return Err(CodecError::SchemaMismatch {
128 expected: expected_hash,
129 found: header.schema_hash,
130 });
131 }
132
133 let mut entities: Vec<EntitySnapshot> = Vec::new();
134 let mut create_seen = false;
135 for section in &packet.sections {
136 match section.tag {
137 SectionTag::EntityCreate => {
138 if create_seen {
139 return Err(CodecError::DuplicateSection {
140 section: section.tag,
141 });
142 }
143 create_seen = true;
144 let decoded = decode_create_section(schema, section.body, limits)?;
145 entities = decoded;
146 }
147 _ => {
148 return Err(CodecError::UnexpectedSection {
149 section: section.tag,
150 });
151 }
152 }
153 }
154
155 Ok(Snapshot {
156 tick: SnapshotTick::new(header.tick),
157 entities,
158 })
159}
160
161pub(crate) fn write_section<F>(
162 tag: SectionTag,
163 out: &mut [u8],
164 limits: &CodecLimits,
165 write_body: F,
166) -> CodecResult<usize>
167where
168 F: FnOnce(&mut BitWriter<'_>) -> CodecResult<()>,
169{
170 if out.len() < 1 + VARINT_MAX_BYTES {
171 return Err(CodecError::OutputTooSmall {
172 needed: 1 + VARINT_MAX_BYTES,
173 available: out.len(),
174 });
175 }
176
177 let body_start = 1 + VARINT_MAX_BYTES;
178 let mut writer = BitWriter::new(&mut out[body_start..]);
179 write_body(&mut writer)?;
180 let body_len = writer.finish();
181
182 if body_len > limits.max_section_bytes {
183 return Err(CodecError::LimitsExceeded {
184 kind: LimitKind::SectionBytes,
185 limit: limits.max_section_bytes,
186 actual: body_len,
187 });
188 }
189
190 let len_u32 = u32::try_from(body_len).map_err(|_| CodecError::OutputTooSmall {
191 needed: body_len,
192 available: out.len(),
193 })?;
194 let len_bytes = varu32_len(len_u32);
195 let total_needed = 1 + len_bytes + body_len;
196 if out.len() < total_needed {
197 return Err(CodecError::OutputTooSmall {
198 needed: total_needed,
199 available: out.len(),
200 });
201 }
202
203 out[0] = tag as u8;
204 write_varu32(len_u32, &mut out[1..1 + len_bytes]);
205 let shift = VARINT_MAX_BYTES - len_bytes;
206 if shift > 0 {
207 let src = body_start..body_start + body_len;
208 out.copy_within(src, 1 + len_bytes);
209 }
210 Ok(total_needed)
211}
212
213fn encode_create_body(
214 schema: &schema::Schema,
215 entities: &[EntitySnapshot],
216 limits: &CodecLimits,
217 writer: &mut BitWriter<'_>,
218) -> CodecResult<()> {
219 if schema.components.len() > limits.max_components_per_entity {
220 return Err(CodecError::LimitsExceeded {
221 kind: LimitKind::ComponentsPerEntity,
222 limit: limits.max_components_per_entity,
223 actual: schema.components.len(),
224 });
225 }
226
227 writer.align_to_byte()?;
228 writer.write_varu32(entities.len() as u32)?;
229
230 let mut prev_id: Option<u32> = None;
231 for entity in entities {
232 if let Some(prev) = prev_id {
233 if entity.id.raw() <= prev {
234 return Err(CodecError::InvalidEntityOrder {
235 previous: prev,
236 current: entity.id.raw(),
237 });
238 }
239 }
240 prev_id = Some(entity.id.raw());
241
242 writer.align_to_byte()?;
243 writer.write_u32_aligned(entity.id.raw())?;
244
245 if entity.components.len() > limits.max_components_per_entity {
246 return Err(CodecError::LimitsExceeded {
247 kind: LimitKind::ComponentsPerEntity,
248 limit: limits.max_components_per_entity,
249 actual: entity.components.len(),
250 });
251 }
252
253 ensure_known_components(schema, entity)?;
254
255 write_component_mask(schema, entity, writer)?;
256
257 for component in schema.components.iter() {
258 if let Some(snapshot) = find_component(entity, component.id) {
259 write_component_fields(component, snapshot, limits, writer)?;
260 }
261 }
262 }
263
264 writer.align_to_byte()?;
265 Ok(())
266}
267
268fn write_component_mask(
269 schema: &schema::Schema,
270 entity: &EntitySnapshot,
271 writer: &mut BitWriter<'_>,
272) -> CodecResult<()> {
273 for component in &schema.components {
274 let present = find_component(entity, component.id).is_some();
275 writer.write_bit(present)?;
276 }
277 Ok(())
278}
279
280fn write_component_fields(
281 component: &ComponentDef,
282 snapshot: &ComponentSnapshot,
283 limits: &CodecLimits,
284 writer: &mut BitWriter<'_>,
285) -> CodecResult<()> {
286 if component.fields.len() > limits.max_fields_per_component {
287 return Err(CodecError::LimitsExceeded {
288 kind: LimitKind::FieldsPerComponent,
289 limit: limits.max_fields_per_component,
290 actual: component.fields.len(),
291 });
292 }
293 if snapshot.fields.len() != component.fields.len() {
294 return Err(CodecError::InvalidMask {
295 kind: MaskKind::FieldMask {
296 component: component.id,
297 },
298 reason: MaskReason::FieldCountMismatch {
299 expected: component.fields.len(),
300 actual: snapshot.fields.len(),
301 },
302 });
303 }
304 if snapshot.fields.len() > limits.max_fields_per_component {
305 return Err(CodecError::LimitsExceeded {
306 kind: LimitKind::FieldsPerComponent,
307 limit: limits.max_fields_per_component,
308 actual: snapshot.fields.len(),
309 });
310 }
311
312 for _field in &component.fields {
313 writer.write_bit(true)?;
314 }
315
316 for (field, value) in component.fields.iter().zip(snapshot.fields.iter()) {
317 write_field_value(component.id, *field, *value, writer)?;
318 }
319 Ok(())
320}
321
322pub(crate) fn write_field_value(
323 component_id: ComponentId,
324 field: FieldDef,
325 value: FieldValue,
326 writer: &mut BitWriter<'_>,
327) -> CodecResult<()> {
328 match (field.codec, value) {
329 (FieldCodec::Bool, FieldValue::Bool(v)) => writer.write_bit(v)?,
330 (FieldCodec::UInt { bits }, FieldValue::UInt(v)) => {
331 validate_uint(component_id, field.id, bits, v)?;
332 writer.write_bits(v, bits)?;
333 }
334 (FieldCodec::SInt { bits }, FieldValue::SInt(v)) => {
335 let encoded = encode_sint(component_id, field.id, bits, v)?;
336 writer.write_bits(encoded, bits)?;
337 }
338 (FieldCodec::VarUInt, FieldValue::VarUInt(v)) => {
339 if v > u32::MAX as u64 {
340 return Err(CodecError::InvalidValue {
341 component: component_id,
342 field: field.id,
343 reason: ValueReason::VarUIntOutOfRange { value: v },
344 });
345 }
346 writer.align_to_byte()?;
347 writer.write_varu32(v as u32)?;
348 }
349 (FieldCodec::VarSInt, FieldValue::VarSInt(v)) => {
350 if v < i32::MIN as i64 || v > i32::MAX as i64 {
351 return Err(CodecError::InvalidValue {
352 component: component_id,
353 field: field.id,
354 reason: ValueReason::VarSIntOutOfRange { value: v },
355 });
356 }
357 writer.align_to_byte()?;
358 writer.write_vars32(v as i32)?;
359 }
360 (FieldCodec::FixedPoint(fp), FieldValue::FixedPoint(v)) => {
361 if v < fp.min_q || v > fp.max_q {
362 return Err(CodecError::InvalidValue {
363 component: component_id,
364 field: field.id,
365 reason: ValueReason::FixedPointOutOfRange {
366 min_q: fp.min_q,
367 max_q: fp.max_q,
368 value: v,
369 },
370 });
371 }
372 let offset = (v - fp.min_q) as u64;
373 let range = (fp.max_q - fp.min_q) as u64;
374 let bits = required_bits(range);
375 if bits > 0 {
376 writer.write_bits(offset, bits)?;
377 }
378 }
379 _ => {
380 return Err(CodecError::InvalidValue {
381 component: component_id,
382 field: field.id,
383 reason: ValueReason::TypeMismatch {
384 expected: codec_name(field.codec),
385 found: value_name(value),
386 },
387 });
388 }
389 }
390 Ok(())
391}
392
393fn decode_create_section(
394 schema: &schema::Schema,
395 body: &[u8],
396 limits: &CodecLimits,
397) -> CodecResult<Vec<EntitySnapshot>> {
398 if body.len() > limits.max_section_bytes {
399 return Err(CodecError::LimitsExceeded {
400 kind: LimitKind::SectionBytes,
401 limit: limits.max_section_bytes,
402 actual: body.len(),
403 });
404 }
405
406 let mut reader = BitReader::new(body);
407 reader.align_to_byte()?;
408 let count = reader.read_varu32()? as usize;
409
410 if count > limits.max_entities_create {
411 return Err(CodecError::LimitsExceeded {
412 kind: LimitKind::EntitiesCreate,
413 limit: limits.max_entities_create,
414 actual: count,
415 });
416 }
417
418 if schema.components.len() > limits.max_components_per_entity {
419 return Err(CodecError::LimitsExceeded {
420 kind: LimitKind::ComponentsPerEntity,
421 limit: limits.max_components_per_entity,
422 actual: schema.components.len(),
423 });
424 }
425
426 let mut entities = Vec::with_capacity(count);
427 let mut prev_id: Option<u32> = None;
428 for _ in 0..count {
429 reader.align_to_byte()?;
430 let entity_id = reader.read_u32_aligned()?;
431 if let Some(prev) = prev_id {
432 if entity_id <= prev {
433 return Err(CodecError::InvalidEntityOrder {
434 previous: prev,
435 current: entity_id,
436 });
437 }
438 }
439 prev_id = Some(entity_id);
440
441 let component_mask = read_mask(
442 &mut reader,
443 schema.components.len(),
444 MaskKind::ComponentMask,
445 )?;
446
447 let mut components = Vec::new();
448 for (idx, component) in schema.components.iter().enumerate() {
449 if component_mask[idx] {
450 let fields = decode_component_fields(component, &mut reader, limits)?;
451 components.push(ComponentSnapshot {
452 id: component.id,
453 fields,
454 });
455 }
456 }
457
458 entities.push(EntitySnapshot {
459 id: EntityId::new(entity_id),
460 components,
461 });
462 }
463
464 reader.align_to_byte()?;
465 let remaining_bits = reader.bits_remaining();
466 if remaining_bits != 0 {
467 return Err(CodecError::TrailingSectionData {
468 section: SectionTag::EntityCreate,
469 remaining_bits,
470 });
471 }
472
473 Ok(entities)
474}
475
476fn decode_component_fields(
477 component: &ComponentDef,
478 reader: &mut BitReader<'_>,
479 limits: &CodecLimits,
480) -> CodecResult<Vec<FieldValue>> {
481 if component.fields.len() > limits.max_fields_per_component {
482 return Err(CodecError::LimitsExceeded {
483 kind: LimitKind::FieldsPerComponent,
484 limit: limits.max_fields_per_component,
485 actual: component.fields.len(),
486 });
487 }
488
489 let mask = read_mask(
490 reader,
491 component.fields.len(),
492 MaskKind::FieldMask {
493 component: component.id,
494 },
495 )?;
496
497 let mut values = Vec::with_capacity(component.fields.len());
498 for (idx, field) in component.fields.iter().enumerate() {
499 if !mask[idx] {
500 return Err(CodecError::InvalidMask {
501 kind: MaskKind::FieldMask {
502 component: component.id,
503 },
504 reason: MaskReason::MissingField { field: field.id },
505 });
506 }
507 let value = read_field_value(component.id, *field, reader)?;
508 values.push(value);
509 }
510 Ok(values)
511}
512
513pub(crate) fn read_field_value(
514 component_id: ComponentId,
515 field: FieldDef,
516 reader: &mut BitReader<'_>,
517) -> CodecResult<FieldValue> {
518 match field.codec {
519 FieldCodec::Bool => Ok(FieldValue::Bool(reader.read_bit()?)),
520 FieldCodec::UInt { bits } => {
521 let value = reader.read_bits(bits)?;
522 validate_uint(component_id, field.id, bits, value)?;
523 Ok(FieldValue::UInt(value))
524 }
525 FieldCodec::SInt { bits } => {
526 let raw = reader.read_bits(bits)?;
527 let value = decode_sint(bits, raw)?;
528 Ok(FieldValue::SInt(value))
529 }
530 FieldCodec::VarUInt => {
531 reader.align_to_byte()?;
532 let value = reader.read_varu32()? as u64;
533 Ok(FieldValue::VarUInt(value))
534 }
535 FieldCodec::VarSInt => {
536 reader.align_to_byte()?;
537 let value = reader.read_vars32()? as i64;
538 Ok(FieldValue::VarSInt(value))
539 }
540 FieldCodec::FixedPoint(fp) => {
541 let range = (fp.max_q - fp.min_q) as u64;
542 let bits = required_bits(range);
543 let offset = if bits == 0 {
544 0
545 } else {
546 reader.read_bits(bits)?
547 };
548 let value = fp.min_q + offset as i64;
549 if value < fp.min_q || value > fp.max_q {
550 return Err(CodecError::InvalidValue {
551 component: component_id,
552 field: field.id,
553 reason: ValueReason::FixedPointOutOfRange {
554 min_q: fp.min_q,
555 max_q: fp.max_q,
556 value,
557 },
558 });
559 }
560 Ok(FieldValue::FixedPoint(value))
561 }
562 }
563}
564
565pub(crate) fn read_mask(
566 reader: &mut BitReader<'_>,
567 expected_bits: usize,
568 kind: MaskKind,
569) -> CodecResult<Vec<bool>> {
570 if reader.bits_remaining() < expected_bits {
571 return Err(CodecError::InvalidMask {
572 kind,
573 reason: MaskReason::NotEnoughBits {
574 expected: expected_bits,
575 available: reader.bits_remaining(),
576 },
577 });
578 }
579
580 let mut mask = Vec::with_capacity(expected_bits);
581 for _ in 0..expected_bits {
582 mask.push(reader.read_bit()?);
583 }
584 Ok(mask)
585}
586
587pub(crate) fn ensure_known_components(
588 schema: &schema::Schema,
589 entity: &EntitySnapshot,
590) -> CodecResult<()> {
591 for component in &entity.components {
592 if schema.components.iter().all(|c| c.id != component.id) {
593 return Err(CodecError::InvalidMask {
594 kind: MaskKind::ComponentMask,
595 reason: MaskReason::UnknownComponent {
596 component: component.id,
597 },
598 });
599 }
600 }
601 Ok(())
602}
603
604fn find_component(entity: &EntitySnapshot, id: ComponentId) -> Option<&ComponentSnapshot> {
605 entity.components.iter().find(|c| c.id == id)
606}
607
608fn validate_uint(
609 component_id: ComponentId,
610 field_id: FieldId,
611 bits: u8,
612 value: u64,
613) -> CodecResult<()> {
614 if bits == 64 {
615 return Ok(());
616 }
617 let max = 1u128 << bits;
618 if value as u128 >= max {
619 return Err(CodecError::InvalidValue {
620 component: component_id,
621 field: field_id,
622 reason: ValueReason::UnsignedOutOfRange { bits, value },
623 });
624 }
625 Ok(())
626}
627
628fn encode_sint(
629 component_id: ComponentId,
630 field_id: FieldId,
631 bits: u8,
632 value: i64,
633) -> CodecResult<u64> {
634 if bits == 64 {
635 return Ok(value as u64);
636 }
637 let min = -(1i128 << (bits - 1));
638 let max = (1i128 << (bits - 1)) - 1;
639 let value_i128 = value as i128;
640 if value_i128 < min || value_i128 > max {
641 return Err(CodecError::InvalidValue {
642 component: component_id,
643 field: field_id,
644 reason: ValueReason::SignedOutOfRange { bits, value },
645 });
646 }
647 let mask = (1u64 << bits) - 1;
648 Ok((value as u64) & mask)
649}
650
651fn decode_sint(bits: u8, raw: u64) -> CodecResult<i64> {
652 if bits == 64 {
653 return Ok(raw as i64);
654 }
655 if bits == 0 {
656 return Ok(0);
657 }
658 let sign_bit = 1u64 << (bits - 1);
659 if raw & sign_bit == 0 {
660 Ok(raw as i64)
661 } else {
662 let mask = (1u64 << bits) - 1;
663 let value = (raw & mask) as i64;
664 Ok(value - (1i64 << bits))
665 }
666}
667
668pub(crate) fn required_bits(range: u64) -> u8 {
669 if range == 0 {
670 return 0;
671 }
672 (64 - range.leading_zeros()) as u8
673}
674
675fn codec_name(codec: FieldCodec) -> &'static str {
676 match codec {
677 FieldCodec::Bool => "bool",
678 FieldCodec::UInt { .. } => "uint",
679 FieldCodec::SInt { .. } => "sint",
680 FieldCodec::VarUInt => "varuint",
681 FieldCodec::VarSInt => "varsint",
682 FieldCodec::FixedPoint(_) => "fixed-point",
683 }
684}
685
686fn value_name(value: FieldValue) -> &'static str {
687 match value {
688 FieldValue::Bool(_) => "bool",
689 FieldValue::UInt(_) => "uint",
690 FieldValue::SInt(_) => "sint",
691 FieldValue::VarUInt(_) => "varuint",
692 FieldValue::VarSInt(_) => "varsint",
693 FieldValue::FixedPoint(_) => "fixed-point",
694 }
695}
696
697fn varu32_len(mut value: u32) -> usize {
698 let mut len = 1;
699 while value >= 0x80 {
700 value >>= 7;
701 len += 1;
702 }
703 len
704}
705
706fn write_varu32(mut value: u32, out: &mut [u8]) {
707 let mut offset = 0;
708 loop {
709 let mut byte = (value & 0x7F) as u8;
710 value >>= 7;
711 if value != 0 {
712 byte |= 0x80;
713 }
714 out[offset] = byte;
715 offset += 1;
716 if value == 0 {
717 break;
718 }
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725 use schema::{ComponentDef, FieldCodec, FieldDef, FieldId, Schema};
726
727 fn schema_one_bool() -> Schema {
728 let component = ComponentDef::new(ComponentId::new(1).unwrap())
729 .field(FieldDef::new(FieldId::new(1).unwrap(), FieldCodec::bool()));
730 Schema::new(vec![component]).unwrap()
731 }
732
733 fn schema_bool_uint10() -> Schema {
734 let component = ComponentDef::new(ComponentId::new(1).unwrap())
735 .field(FieldDef::new(FieldId::new(1).unwrap(), FieldCodec::bool()))
736 .field(FieldDef::new(
737 FieldId::new(2).unwrap(),
738 FieldCodec::uint(10),
739 ));
740 Schema::new(vec![component]).unwrap()
741 }
742
743 #[test]
744 fn full_snapshot_roundtrip_minimal() {
745 let schema = schema_one_bool();
746 let snapshot = Snapshot {
747 tick: SnapshotTick::new(1),
748 entities: vec![EntitySnapshot {
749 id: EntityId::new(1),
750 components: vec![ComponentSnapshot {
751 id: ComponentId::new(1).unwrap(),
752 fields: vec![FieldValue::Bool(true)],
753 }],
754 }],
755 };
756
757 let mut buf = [0u8; 128];
758 let bytes = encode_full_snapshot(
759 &schema,
760 snapshot.tick,
761 &snapshot.entities,
762 &CodecLimits::for_testing(),
763 &mut buf,
764 )
765 .unwrap();
766 let decoded = decode_full_snapshot(
767 &schema,
768 &buf[..bytes],
769 &wire::Limits::for_testing(),
770 &CodecLimits::for_testing(),
771 )
772 .unwrap();
773 assert_eq!(decoded.entities, snapshot.entities);
774 }
775
776 #[test]
777 fn full_snapshot_golden_bytes() {
778 let schema = schema_one_bool();
779 let entities = vec![EntitySnapshot {
780 id: EntityId::new(1),
781 components: vec![ComponentSnapshot {
782 id: ComponentId::new(1).unwrap(),
783 fields: vec![FieldValue::Bool(true)],
784 }],
785 }];
786
787 let mut buf = [0u8; 128];
788 let bytes = encode_full_snapshot(
789 &schema,
790 SnapshotTick::new(1),
791 &entities,
792 &CodecLimits::for_testing(),
793 &mut buf,
794 )
795 .unwrap();
796
797 let mut expected = Vec::new();
798 expected.extend_from_slice(&wire::MAGIC.to_le_bytes());
799 expected.extend_from_slice(&wire::VERSION.to_le_bytes());
800 expected.extend_from_slice(&wire::PacketFlags::full_snapshot().raw().to_le_bytes());
801 expected.extend_from_slice(&0x32F5_A224_657B_EE15u64.to_le_bytes());
802 expected.extend_from_slice(&1u32.to_le_bytes());
803 expected.extend_from_slice(&0u32.to_le_bytes());
804 expected.extend_from_slice(&8u32.to_le_bytes());
805 expected.extend_from_slice(&[SectionTag::EntityCreate as u8, 6, 1, 1, 0, 0, 0, 0xE0]);
806
807 assert_eq!(&buf[..bytes], expected.as_slice());
808 }
809
810 #[test]
811 fn full_snapshot_golden_fixture_two_fields() {
812 let schema = schema_bool_uint10();
813 let entities = vec![EntitySnapshot {
814 id: EntityId::new(1),
815 components: vec![ComponentSnapshot {
816 id: ComponentId::new(1).unwrap(),
817 fields: vec![FieldValue::Bool(true), FieldValue::UInt(513)],
818 }],
819 }];
820
821 let mut buf = [0u8; 128];
822 let bytes = encode_full_snapshot(
823 &schema,
824 SnapshotTick::new(1),
825 &entities,
826 &CodecLimits::for_testing(),
827 &mut buf,
828 )
829 .unwrap();
830
831 let mut expected = Vec::new();
832 expected.extend_from_slice(&wire::MAGIC.to_le_bytes());
833 expected.extend_from_slice(&wire::VERSION.to_le_bytes());
834 expected.extend_from_slice(&wire::PacketFlags::full_snapshot().raw().to_le_bytes());
835 expected.extend_from_slice(&0x57B2_2433_26F2_2706u64.to_le_bytes());
836 expected.extend_from_slice(&1u32.to_le_bytes());
837 expected.extend_from_slice(&0u32.to_le_bytes());
838 expected.extend_from_slice(&9u32.to_le_bytes());
839 expected.extend_from_slice(&[SectionTag::EntityCreate as u8, 7, 1, 1, 0, 0, 0, 0xF8, 0x04]);
840
841 assert_eq!(&buf[..bytes], expected.as_slice());
842 }
843
844 #[test]
845 fn decode_rejects_trailing_bytes() {
846 let schema = schema_one_bool();
847 let entities = vec![EntitySnapshot {
848 id: EntityId::new(1),
849 components: vec![ComponentSnapshot {
850 id: ComponentId::new(1).unwrap(),
851 fields: vec![FieldValue::Bool(true)],
852 }],
853 }];
854
855 let mut buf = [0u8; 128];
856 let bytes = encode_full_snapshot(
857 &schema,
858 SnapshotTick::new(1),
859 &entities,
860 &CodecLimits::for_testing(),
861 &mut buf,
862 )
863 .unwrap();
864
865 let mut extra = buf[..bytes].to_vec();
867 extra[wire::HEADER_SIZE + 1] = 7; let payload_len = 9u32;
869 extra[24..28].copy_from_slice(&payload_len.to_le_bytes());
870 extra.push(0);
871
872 let err = decode_full_snapshot(
873 &schema,
874 &extra,
875 &wire::Limits::for_testing(),
876 &CodecLimits::for_testing(),
877 )
878 .unwrap_err();
879 assert!(matches!(err, CodecError::TrailingSectionData { .. }));
880 }
881
882 #[test]
883 fn decode_rejects_excessive_entity_count_early() {
884 let schema = schema_one_bool();
885 let limits = CodecLimits::for_testing();
886 let count = (limits.max_entities_create as u32) + 1;
887
888 let mut body = [0u8; 8];
889 write_varu32(count, &mut body);
890 let body_len = varu32_len(count);
891 let mut section_buf = [0u8; 16];
892 let section_len = wire::encode_section(
893 SectionTag::EntityCreate,
894 &body[..body_len],
895 &mut section_buf,
896 )
897 .unwrap();
898
899 let payload_len = section_len as u32;
900 let header = wire::PacketHeader::full_snapshot(schema_hash(&schema), 1, payload_len);
901 let mut buf = [0u8; wire::HEADER_SIZE + 16];
902 encode_header(&header, &mut buf[..wire::HEADER_SIZE]).unwrap();
903 buf[wire::HEADER_SIZE..wire::HEADER_SIZE + section_len]
904 .copy_from_slice(§ion_buf[..section_len]);
905 let buf = &buf[..wire::HEADER_SIZE + section_len];
906
907 let err =
908 decode_full_snapshot(&schema, buf, &wire::Limits::for_testing(), &limits).unwrap_err();
909 assert!(matches!(
910 err,
911 CodecError::LimitsExceeded {
912 kind: LimitKind::EntitiesCreate,
913 ..
914 }
915 ));
916 }
917
918 #[test]
919 fn decode_rejects_truncated_prefixes() {
920 let schema = schema_one_bool();
921 let entities = vec![EntitySnapshot {
922 id: EntityId::new(1),
923 components: vec![ComponentSnapshot {
924 id: ComponentId::new(1).unwrap(),
925 fields: vec![FieldValue::Bool(true)],
926 }],
927 }];
928
929 let mut buf = [0u8; 128];
930 let bytes = encode_full_snapshot(
931 &schema,
932 SnapshotTick::new(1),
933 &entities,
934 &CodecLimits::for_testing(),
935 &mut buf,
936 )
937 .unwrap();
938
939 for len in 0..bytes {
940 let result = decode_full_snapshot(
941 &schema,
942 &buf[..len],
943 &wire::Limits::for_testing(),
944 &CodecLimits::for_testing(),
945 );
946 assert!(result.is_err());
947 }
948 }
949
950 #[test]
951 fn encode_is_deterministic_for_same_input() {
952 let schema = schema_one_bool();
953 let entities = vec![EntitySnapshot {
954 id: EntityId::new(1),
955 components: vec![ComponentSnapshot {
956 id: ComponentId::new(1).unwrap(),
957 fields: vec![FieldValue::Bool(true)],
958 }],
959 }];
960
961 let mut buf1 = [0u8; 128];
962 let mut buf2 = [0u8; 128];
963 let bytes1 = encode_full_snapshot(
964 &schema,
965 SnapshotTick::new(1),
966 &entities,
967 &CodecLimits::for_testing(),
968 &mut buf1,
969 )
970 .unwrap();
971 let bytes2 = encode_full_snapshot(
972 &schema,
973 SnapshotTick::new(1),
974 &entities,
975 &CodecLimits::for_testing(),
976 &mut buf2,
977 )
978 .unwrap();
979
980 assert_eq!(&buf1[..bytes1], &buf2[..bytes2]);
981 }
982
983 #[test]
984 fn decode_rejects_missing_field_mask() {
985 let schema = schema_one_bool();
986 let entities = vec![EntitySnapshot {
987 id: EntityId::new(1),
988 components: vec![ComponentSnapshot {
989 id: ComponentId::new(1).unwrap(),
990 fields: vec![FieldValue::Bool(true)],
991 }],
992 }];
993
994 let mut buf = [0u8; 128];
995 let bytes = encode_full_snapshot(
996 &schema,
997 SnapshotTick::new(1),
998 &entities,
999 &CodecLimits::for_testing(),
1000 &mut buf,
1001 )
1002 .unwrap();
1003
1004 let payload_start = wire::HEADER_SIZE;
1006 let mask_offset = payload_start + 2 + 1 + 4; buf[mask_offset] &= 0b1011_1111;
1008
1009 let err = decode_full_snapshot(
1010 &schema,
1011 &buf[..bytes],
1012 &wire::Limits::for_testing(),
1013 &CodecLimits::for_testing(),
1014 )
1015 .unwrap_err();
1016 assert!(matches!(err, CodecError::InvalidMask { .. }));
1017 }
1018
1019 #[test]
1020 fn encode_rejects_unsorted_entities() {
1021 let schema = schema_one_bool();
1022 let entities = vec![
1023 EntitySnapshot {
1024 id: EntityId::new(2),
1025 components: vec![ComponentSnapshot {
1026 id: ComponentId::new(1).unwrap(),
1027 fields: vec![FieldValue::Bool(true)],
1028 }],
1029 },
1030 EntitySnapshot {
1031 id: EntityId::new(1),
1032 components: vec![ComponentSnapshot {
1033 id: ComponentId::new(1).unwrap(),
1034 fields: vec![FieldValue::Bool(false)],
1035 }],
1036 },
1037 ];
1038
1039 let mut buf = [0u8; 128];
1040 let err = encode_full_snapshot(
1041 &schema,
1042 SnapshotTick::new(1),
1043 &entities,
1044 &CodecLimits::for_testing(),
1045 &mut buf,
1046 )
1047 .unwrap_err();
1048 assert!(matches!(err, CodecError::InvalidEntityOrder { .. }));
1049 }
1050}