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
393pub(crate) fn write_field_value_sparse(
394 component_id: ComponentId,
395 field: FieldDef,
396 value: FieldValue,
397 writer: &mut BitWriter<'_>,
398) -> CodecResult<()> {
399 match (field.codec, value) {
400 (FieldCodec::Bool, FieldValue::Bool(v)) => writer.write_bit(v)?,
401 (FieldCodec::UInt { bits }, FieldValue::UInt(v)) => {
402 validate_uint(component_id, field.id, bits, v)?;
403 writer.write_bits(v, bits)?;
404 }
405 (FieldCodec::SInt { bits }, FieldValue::SInt(v)) => {
406 let encoded = encode_sint(component_id, field.id, bits, v)?;
407 writer.write_bits(encoded, bits)?;
408 }
409 (FieldCodec::VarUInt, FieldValue::VarUInt(v)) => {
410 if v > u32::MAX as u64 {
411 return Err(CodecError::InvalidValue {
412 component: component_id,
413 field: field.id,
414 reason: ValueReason::VarUIntOutOfRange { value: v },
415 });
416 }
417 writer.align_to_byte()?;
418 writer.write_varu32(v as u32)?;
419 }
420 (FieldCodec::VarSInt, FieldValue::VarSInt(v)) => {
421 if v < i32::MIN as i64 || v > i32::MAX as i64 {
422 return Err(CodecError::InvalidValue {
423 component: component_id,
424 field: field.id,
425 reason: ValueReason::VarSIntOutOfRange { value: v },
426 });
427 }
428 writer.align_to_byte()?;
429 writer.write_vars32(v as i32)?;
430 }
431 (FieldCodec::FixedPoint(fp), FieldValue::FixedPoint(v)) => {
432 if v < fp.min_q || v > fp.max_q {
433 return Err(CodecError::InvalidValue {
434 component: component_id,
435 field: field.id,
436 reason: ValueReason::FixedPointOutOfRange {
437 min_q: fp.min_q,
438 max_q: fp.max_q,
439 value: v,
440 },
441 });
442 }
443 let offset = (v - fp.min_q) as u64;
444 let range = (fp.max_q - fp.min_q) as u64;
445 let bits = required_bits(range);
446 if bits > 0 {
447 writer.write_bits(offset, bits)?;
448 }
449 }
450 _ => {
451 return Err(CodecError::InvalidValue {
452 component: component_id,
453 field: field.id,
454 reason: ValueReason::TypeMismatch {
455 expected: codec_name(field.codec),
456 found: value_name(value),
457 },
458 });
459 }
460 }
461 Ok(())
462}
463
464fn decode_create_section(
465 schema: &schema::Schema,
466 body: &[u8],
467 limits: &CodecLimits,
468) -> CodecResult<Vec<EntitySnapshot>> {
469 if body.len() > limits.max_section_bytes {
470 return Err(CodecError::LimitsExceeded {
471 kind: LimitKind::SectionBytes,
472 limit: limits.max_section_bytes,
473 actual: body.len(),
474 });
475 }
476
477 let mut reader = BitReader::new(body);
478 reader.align_to_byte()?;
479 let count = reader.read_varu32()? as usize;
480
481 if count > limits.max_entities_create {
482 return Err(CodecError::LimitsExceeded {
483 kind: LimitKind::EntitiesCreate,
484 limit: limits.max_entities_create,
485 actual: count,
486 });
487 }
488
489 if schema.components.len() > limits.max_components_per_entity {
490 return Err(CodecError::LimitsExceeded {
491 kind: LimitKind::ComponentsPerEntity,
492 limit: limits.max_components_per_entity,
493 actual: schema.components.len(),
494 });
495 }
496
497 let mut entities = Vec::with_capacity(count);
498 let mut prev_id: Option<u32> = None;
499 for _ in 0..count {
500 reader.align_to_byte()?;
501 let entity_id = reader.read_u32_aligned()?;
502 if let Some(prev) = prev_id {
503 if entity_id <= prev {
504 return Err(CodecError::InvalidEntityOrder {
505 previous: prev,
506 current: entity_id,
507 });
508 }
509 }
510 prev_id = Some(entity_id);
511
512 let component_mask = read_mask(
513 &mut reader,
514 schema.components.len(),
515 MaskKind::ComponentMask,
516 )?;
517
518 let mut components = Vec::new();
519 for (idx, component) in schema.components.iter().enumerate() {
520 if component_mask[idx] {
521 let fields = decode_component_fields(component, &mut reader, limits)?;
522 components.push(ComponentSnapshot {
523 id: component.id,
524 fields,
525 });
526 }
527 }
528
529 entities.push(EntitySnapshot {
530 id: EntityId::new(entity_id),
531 components,
532 });
533 }
534
535 reader.align_to_byte()?;
536 let remaining_bits = reader.bits_remaining();
537 if remaining_bits != 0 {
538 return Err(CodecError::TrailingSectionData {
539 section: SectionTag::EntityCreate,
540 remaining_bits,
541 });
542 }
543
544 Ok(entities)
545}
546
547fn decode_component_fields(
548 component: &ComponentDef,
549 reader: &mut BitReader<'_>,
550 limits: &CodecLimits,
551) -> CodecResult<Vec<FieldValue>> {
552 if component.fields.len() > limits.max_fields_per_component {
553 return Err(CodecError::LimitsExceeded {
554 kind: LimitKind::FieldsPerComponent,
555 limit: limits.max_fields_per_component,
556 actual: component.fields.len(),
557 });
558 }
559
560 let mask = read_mask(
561 reader,
562 component.fields.len(),
563 MaskKind::FieldMask {
564 component: component.id,
565 },
566 )?;
567
568 let mut values = Vec::with_capacity(component.fields.len());
569 for (idx, field) in component.fields.iter().enumerate() {
570 if !mask[idx] {
571 return Err(CodecError::InvalidMask {
572 kind: MaskKind::FieldMask {
573 component: component.id,
574 },
575 reason: MaskReason::MissingField { field: field.id },
576 });
577 }
578 let value = read_field_value(component.id, *field, reader)?;
579 values.push(value);
580 }
581 Ok(values)
582}
583
584pub(crate) fn read_field_value(
585 component_id: ComponentId,
586 field: FieldDef,
587 reader: &mut BitReader<'_>,
588) -> CodecResult<FieldValue> {
589 match field.codec {
590 FieldCodec::Bool => Ok(FieldValue::Bool(reader.read_bit()?)),
591 FieldCodec::UInt { bits } => {
592 let value = reader.read_bits(bits)?;
593 validate_uint(component_id, field.id, bits, value)?;
594 Ok(FieldValue::UInt(value))
595 }
596 FieldCodec::SInt { bits } => {
597 let raw = reader.read_bits(bits)?;
598 let value = decode_sint(bits, raw)?;
599 Ok(FieldValue::SInt(value))
600 }
601 FieldCodec::VarUInt => {
602 reader.align_to_byte()?;
603 let value = reader.read_varu32()? as u64;
604 Ok(FieldValue::VarUInt(value))
605 }
606 FieldCodec::VarSInt => {
607 reader.align_to_byte()?;
608 let value = reader.read_vars32()? as i64;
609 Ok(FieldValue::VarSInt(value))
610 }
611 FieldCodec::FixedPoint(fp) => {
612 let range = (fp.max_q - fp.min_q) as u64;
613 let bits = required_bits(range);
614 let offset = if bits == 0 {
615 0
616 } else {
617 reader.read_bits(bits)?
618 };
619 let value = fp.min_q + offset as i64;
620 if value < fp.min_q || value > fp.max_q {
621 return Err(CodecError::InvalidValue {
622 component: component_id,
623 field: field.id,
624 reason: ValueReason::FixedPointOutOfRange {
625 min_q: fp.min_q,
626 max_q: fp.max_q,
627 value,
628 },
629 });
630 }
631 Ok(FieldValue::FixedPoint(value))
632 }
633 }
634}
635
636pub(crate) fn read_field_value_sparse(
637 component_id: ComponentId,
638 field: FieldDef,
639 reader: &mut BitReader<'_>,
640) -> CodecResult<FieldValue> {
641 match field.codec {
642 FieldCodec::Bool => Ok(FieldValue::Bool(reader.read_bit()?)),
643 FieldCodec::UInt { bits } => {
644 let value = reader.read_bits(bits)?;
645 validate_uint(component_id, field.id, bits, value)?;
646 Ok(FieldValue::UInt(value))
647 }
648 FieldCodec::SInt { bits } => {
649 let raw = reader.read_bits(bits)?;
650 let value = decode_sint(bits, raw)?;
651 Ok(FieldValue::SInt(value))
652 }
653 FieldCodec::VarUInt => {
654 reader.align_to_byte()?;
655 let value = reader.read_varu32()? as u64;
656 Ok(FieldValue::VarUInt(value))
657 }
658 FieldCodec::VarSInt => {
659 reader.align_to_byte()?;
660 let value = reader.read_vars32()? as i64;
661 Ok(FieldValue::VarSInt(value))
662 }
663 FieldCodec::FixedPoint(fp) => {
664 let range = (fp.max_q - fp.min_q) as u64;
665 let bits = required_bits(range);
666 let offset = if bits == 0 {
667 0
668 } else {
669 reader.read_bits(bits)?
670 };
671 let value = fp.min_q + offset as i64;
672 if value < fp.min_q || value > fp.max_q {
673 return Err(CodecError::InvalidValue {
674 component: component_id,
675 field: field.id,
676 reason: ValueReason::FixedPointOutOfRange {
677 min_q: fp.min_q,
678 max_q: fp.max_q,
679 value,
680 },
681 });
682 }
683 Ok(FieldValue::FixedPoint(value))
684 }
685 }
686}
687
688pub(crate) fn read_mask(
689 reader: &mut BitReader<'_>,
690 expected_bits: usize,
691 kind: MaskKind,
692) -> CodecResult<Vec<bool>> {
693 if reader.bits_remaining() < expected_bits {
694 return Err(CodecError::InvalidMask {
695 kind,
696 reason: MaskReason::NotEnoughBits {
697 expected: expected_bits,
698 available: reader.bits_remaining(),
699 },
700 });
701 }
702
703 let mut mask = Vec::with_capacity(expected_bits);
704 for _ in 0..expected_bits {
705 mask.push(reader.read_bit()?);
706 }
707 Ok(mask)
708}
709
710pub(crate) fn ensure_known_components(
711 schema: &schema::Schema,
712 entity: &EntitySnapshot,
713) -> CodecResult<()> {
714 for component in &entity.components {
715 if schema.components.iter().all(|c| c.id != component.id) {
716 return Err(CodecError::InvalidMask {
717 kind: MaskKind::ComponentMask,
718 reason: MaskReason::UnknownComponent {
719 component: component.id,
720 },
721 });
722 }
723 }
724 Ok(())
725}
726
727fn find_component(entity: &EntitySnapshot, id: ComponentId) -> Option<&ComponentSnapshot> {
728 entity.components.iter().find(|c| c.id == id)
729}
730
731fn validate_uint(
732 component_id: ComponentId,
733 field_id: FieldId,
734 bits: u8,
735 value: u64,
736) -> CodecResult<()> {
737 if bits == 64 {
738 return Ok(());
739 }
740 let max = 1u128 << bits;
741 if value as u128 >= max {
742 return Err(CodecError::InvalidValue {
743 component: component_id,
744 field: field_id,
745 reason: ValueReason::UnsignedOutOfRange { bits, value },
746 });
747 }
748 Ok(())
749}
750
751fn encode_sint(
752 component_id: ComponentId,
753 field_id: FieldId,
754 bits: u8,
755 value: i64,
756) -> CodecResult<u64> {
757 if bits == 64 {
758 return Ok(value as u64);
759 }
760 let min = -(1i128 << (bits - 1));
761 let max = (1i128 << (bits - 1)) - 1;
762 let value_i128 = value as i128;
763 if value_i128 < min || value_i128 > max {
764 return Err(CodecError::InvalidValue {
765 component: component_id,
766 field: field_id,
767 reason: ValueReason::SignedOutOfRange { bits, value },
768 });
769 }
770 let mask = (1u64 << bits) - 1;
771 Ok((value as u64) & mask)
772}
773
774fn decode_sint(bits: u8, raw: u64) -> CodecResult<i64> {
775 if bits == 64 {
776 return Ok(raw as i64);
777 }
778 if bits == 0 {
779 return Ok(0);
780 }
781 let sign_bit = 1u64 << (bits - 1);
782 if raw & sign_bit == 0 {
783 Ok(raw as i64)
784 } else {
785 let mask = (1u64 << bits) - 1;
786 let value = (raw & mask) as i64;
787 Ok(value - (1i64 << bits))
788 }
789}
790
791pub(crate) fn required_bits(range: u64) -> u8 {
792 if range == 0 {
793 return 0;
794 }
795 (64 - range.leading_zeros()) as u8
796}
797
798fn codec_name(codec: FieldCodec) -> &'static str {
799 match codec {
800 FieldCodec::Bool => "bool",
801 FieldCodec::UInt { .. } => "uint",
802 FieldCodec::SInt { .. } => "sint",
803 FieldCodec::VarUInt => "varuint",
804 FieldCodec::VarSInt => "varsint",
805 FieldCodec::FixedPoint(_) => "fixed-point",
806 }
807}
808
809fn value_name(value: FieldValue) -> &'static str {
810 match value {
811 FieldValue::Bool(_) => "bool",
812 FieldValue::UInt(_) => "uint",
813 FieldValue::SInt(_) => "sint",
814 FieldValue::VarUInt(_) => "varuint",
815 FieldValue::VarSInt(_) => "varsint",
816 FieldValue::FixedPoint(_) => "fixed-point",
817 }
818}
819
820fn varu32_len(mut value: u32) -> usize {
821 let mut len = 1;
822 while value >= 0x80 {
823 value >>= 7;
824 len += 1;
825 }
826 len
827}
828
829fn write_varu32(mut value: u32, out: &mut [u8]) {
830 let mut offset = 0;
831 loop {
832 let mut byte = (value & 0x7F) as u8;
833 value >>= 7;
834 if value != 0 {
835 byte |= 0x80;
836 }
837 out[offset] = byte;
838 offset += 1;
839 if value == 0 {
840 break;
841 }
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848 use schema::{ComponentDef, FieldCodec, FieldDef, FieldId, Schema};
849
850 fn schema_one_bool() -> Schema {
851 let component = ComponentDef::new(ComponentId::new(1).unwrap())
852 .field(FieldDef::new(FieldId::new(1).unwrap(), FieldCodec::bool()));
853 Schema::new(vec![component]).unwrap()
854 }
855
856 fn schema_bool_uint10() -> Schema {
857 let component = ComponentDef::new(ComponentId::new(1).unwrap())
858 .field(FieldDef::new(FieldId::new(1).unwrap(), FieldCodec::bool()))
859 .field(FieldDef::new(
860 FieldId::new(2).unwrap(),
861 FieldCodec::uint(10),
862 ));
863 Schema::new(vec![component]).unwrap()
864 }
865
866 #[test]
867 fn full_snapshot_roundtrip_minimal() {
868 let schema = schema_one_bool();
869 let snapshot = Snapshot {
870 tick: SnapshotTick::new(1),
871 entities: vec![EntitySnapshot {
872 id: EntityId::new(1),
873 components: vec![ComponentSnapshot {
874 id: ComponentId::new(1).unwrap(),
875 fields: vec![FieldValue::Bool(true)],
876 }],
877 }],
878 };
879
880 let mut buf = [0u8; 128];
881 let bytes = encode_full_snapshot(
882 &schema,
883 snapshot.tick,
884 &snapshot.entities,
885 &CodecLimits::for_testing(),
886 &mut buf,
887 )
888 .unwrap();
889 let decoded = decode_full_snapshot(
890 &schema,
891 &buf[..bytes],
892 &wire::Limits::for_testing(),
893 &CodecLimits::for_testing(),
894 )
895 .unwrap();
896 assert_eq!(decoded.entities, snapshot.entities);
897 }
898
899 #[test]
900 fn full_snapshot_golden_bytes() {
901 let schema = schema_one_bool();
902 let entities = vec![EntitySnapshot {
903 id: EntityId::new(1),
904 components: vec![ComponentSnapshot {
905 id: ComponentId::new(1).unwrap(),
906 fields: vec![FieldValue::Bool(true)],
907 }],
908 }];
909
910 let mut buf = [0u8; 128];
911 let bytes = encode_full_snapshot(
912 &schema,
913 SnapshotTick::new(1),
914 &entities,
915 &CodecLimits::for_testing(),
916 &mut buf,
917 )
918 .unwrap();
919
920 let mut expected = Vec::new();
921 expected.extend_from_slice(&wire::MAGIC.to_le_bytes());
922 expected.extend_from_slice(&wire::VERSION.to_le_bytes());
923 expected.extend_from_slice(&wire::PacketFlags::full_snapshot().raw().to_le_bytes());
924 expected.extend_from_slice(&0x32F5_A224_657B_EE15u64.to_le_bytes());
925 expected.extend_from_slice(&1u32.to_le_bytes());
926 expected.extend_from_slice(&0u32.to_le_bytes());
927 expected.extend_from_slice(&8u32.to_le_bytes());
928 expected.extend_from_slice(&[SectionTag::EntityCreate as u8, 6, 1, 1, 0, 0, 0, 0xE0]);
929
930 assert_eq!(&buf[..bytes], expected.as_slice());
931 }
932
933 #[test]
934 fn full_snapshot_golden_fixture_two_fields() {
935 let schema = schema_bool_uint10();
936 let entities = vec![EntitySnapshot {
937 id: EntityId::new(1),
938 components: vec![ComponentSnapshot {
939 id: ComponentId::new(1).unwrap(),
940 fields: vec![FieldValue::Bool(true), FieldValue::UInt(513)],
941 }],
942 }];
943
944 let mut buf = [0u8; 128];
945 let bytes = encode_full_snapshot(
946 &schema,
947 SnapshotTick::new(1),
948 &entities,
949 &CodecLimits::for_testing(),
950 &mut buf,
951 )
952 .unwrap();
953
954 let mut expected = Vec::new();
955 expected.extend_from_slice(&wire::MAGIC.to_le_bytes());
956 expected.extend_from_slice(&wire::VERSION.to_le_bytes());
957 expected.extend_from_slice(&wire::PacketFlags::full_snapshot().raw().to_le_bytes());
958 expected.extend_from_slice(&0x57B2_2433_26F2_2706u64.to_le_bytes());
959 expected.extend_from_slice(&1u32.to_le_bytes());
960 expected.extend_from_slice(&0u32.to_le_bytes());
961 expected.extend_from_slice(&9u32.to_le_bytes());
962 expected.extend_from_slice(&[SectionTag::EntityCreate as u8, 7, 1, 1, 0, 0, 0, 0xF8, 0x04]);
963
964 assert_eq!(&buf[..bytes], expected.as_slice());
965 }
966
967 #[test]
968 fn decode_rejects_trailing_bytes() {
969 let schema = schema_one_bool();
970 let entities = vec![EntitySnapshot {
971 id: EntityId::new(1),
972 components: vec![ComponentSnapshot {
973 id: ComponentId::new(1).unwrap(),
974 fields: vec![FieldValue::Bool(true)],
975 }],
976 }];
977
978 let mut buf = [0u8; 128];
979 let bytes = encode_full_snapshot(
980 &schema,
981 SnapshotTick::new(1),
982 &entities,
983 &CodecLimits::for_testing(),
984 &mut buf,
985 )
986 .unwrap();
987
988 let mut extra = buf[..bytes].to_vec();
990 extra[wire::HEADER_SIZE + 1] = 7; let payload_len = 9u32;
992 extra[24..28].copy_from_slice(&payload_len.to_le_bytes());
993 extra.push(0);
994
995 let err = decode_full_snapshot(
996 &schema,
997 &extra,
998 &wire::Limits::for_testing(),
999 &CodecLimits::for_testing(),
1000 )
1001 .unwrap_err();
1002 assert!(matches!(err, CodecError::TrailingSectionData { .. }));
1003 }
1004
1005 #[test]
1006 fn decode_rejects_excessive_entity_count_early() {
1007 let schema = schema_one_bool();
1008 let limits = CodecLimits::for_testing();
1009 let count = (limits.max_entities_create as u32) + 1;
1010
1011 let mut body = [0u8; 8];
1012 write_varu32(count, &mut body);
1013 let body_len = varu32_len(count);
1014 let mut section_buf = [0u8; 16];
1015 let section_len = wire::encode_section(
1016 SectionTag::EntityCreate,
1017 &body[..body_len],
1018 &mut section_buf,
1019 )
1020 .unwrap();
1021
1022 let payload_len = section_len as u32;
1023 let header = wire::PacketHeader::full_snapshot(schema_hash(&schema), 1, payload_len);
1024 let mut buf = [0u8; wire::HEADER_SIZE + 16];
1025 encode_header(&header, &mut buf[..wire::HEADER_SIZE]).unwrap();
1026 buf[wire::HEADER_SIZE..wire::HEADER_SIZE + section_len]
1027 .copy_from_slice(§ion_buf[..section_len]);
1028 let buf = &buf[..wire::HEADER_SIZE + section_len];
1029
1030 let err =
1031 decode_full_snapshot(&schema, buf, &wire::Limits::for_testing(), &limits).unwrap_err();
1032 assert!(matches!(
1033 err,
1034 CodecError::LimitsExceeded {
1035 kind: LimitKind::EntitiesCreate,
1036 ..
1037 }
1038 ));
1039 }
1040
1041 #[test]
1042 fn decode_rejects_truncated_prefixes() {
1043 let schema = schema_one_bool();
1044 let entities = vec![EntitySnapshot {
1045 id: EntityId::new(1),
1046 components: vec![ComponentSnapshot {
1047 id: ComponentId::new(1).unwrap(),
1048 fields: vec![FieldValue::Bool(true)],
1049 }],
1050 }];
1051
1052 let mut buf = [0u8; 128];
1053 let bytes = encode_full_snapshot(
1054 &schema,
1055 SnapshotTick::new(1),
1056 &entities,
1057 &CodecLimits::for_testing(),
1058 &mut buf,
1059 )
1060 .unwrap();
1061
1062 for len in 0..bytes {
1063 let result = decode_full_snapshot(
1064 &schema,
1065 &buf[..len],
1066 &wire::Limits::for_testing(),
1067 &CodecLimits::for_testing(),
1068 );
1069 assert!(result.is_err());
1070 }
1071 }
1072
1073 #[test]
1074 fn encode_is_deterministic_for_same_input() {
1075 let schema = schema_one_bool();
1076 let entities = vec![EntitySnapshot {
1077 id: EntityId::new(1),
1078 components: vec![ComponentSnapshot {
1079 id: ComponentId::new(1).unwrap(),
1080 fields: vec![FieldValue::Bool(true)],
1081 }],
1082 }];
1083
1084 let mut buf1 = [0u8; 128];
1085 let mut buf2 = [0u8; 128];
1086 let bytes1 = encode_full_snapshot(
1087 &schema,
1088 SnapshotTick::new(1),
1089 &entities,
1090 &CodecLimits::for_testing(),
1091 &mut buf1,
1092 )
1093 .unwrap();
1094 let bytes2 = encode_full_snapshot(
1095 &schema,
1096 SnapshotTick::new(1),
1097 &entities,
1098 &CodecLimits::for_testing(),
1099 &mut buf2,
1100 )
1101 .unwrap();
1102
1103 assert_eq!(&buf1[..bytes1], &buf2[..bytes2]);
1104 }
1105
1106 #[test]
1107 fn decode_rejects_missing_field_mask() {
1108 let schema = schema_one_bool();
1109 let entities = vec![EntitySnapshot {
1110 id: EntityId::new(1),
1111 components: vec![ComponentSnapshot {
1112 id: ComponentId::new(1).unwrap(),
1113 fields: vec![FieldValue::Bool(true)],
1114 }],
1115 }];
1116
1117 let mut buf = [0u8; 128];
1118 let bytes = encode_full_snapshot(
1119 &schema,
1120 SnapshotTick::new(1),
1121 &entities,
1122 &CodecLimits::for_testing(),
1123 &mut buf,
1124 )
1125 .unwrap();
1126
1127 let payload_start = wire::HEADER_SIZE;
1129 let mask_offset = payload_start + 2 + 1 + 4; buf[mask_offset] &= 0b1011_1111;
1131
1132 let err = decode_full_snapshot(
1133 &schema,
1134 &buf[..bytes],
1135 &wire::Limits::for_testing(),
1136 &CodecLimits::for_testing(),
1137 )
1138 .unwrap_err();
1139 assert!(matches!(err, CodecError::InvalidMask { .. }));
1140 }
1141
1142 #[test]
1143 fn encode_rejects_unsorted_entities() {
1144 let schema = schema_one_bool();
1145 let entities = vec![
1146 EntitySnapshot {
1147 id: EntityId::new(2),
1148 components: vec![ComponentSnapshot {
1149 id: ComponentId::new(1).unwrap(),
1150 fields: vec![FieldValue::Bool(true)],
1151 }],
1152 },
1153 EntitySnapshot {
1154 id: EntityId::new(1),
1155 components: vec![ComponentSnapshot {
1156 id: ComponentId::new(1).unwrap(),
1157 fields: vec![FieldValue::Bool(false)],
1158 }],
1159 },
1160 ];
1161
1162 let mut buf = [0u8; 128];
1163 let err = encode_full_snapshot(
1164 &schema,
1165 SnapshotTick::new(1),
1166 &entities,
1167 &CodecLimits::for_testing(),
1168 &mut buf,
1169 )
1170 .unwrap_err();
1171 assert!(matches!(err, CodecError::InvalidEntityOrder { .. }));
1172 }
1173}