Skip to main content

codec/
snapshot.rs

1//! Full snapshot encoding/decoding.
2
3use 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/// A decoded snapshot.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Snapshot {
16    pub tick: SnapshotTick,
17    pub entities: Vec<EntitySnapshot>,
18}
19
20/// An entity snapshot.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct EntitySnapshot {
23    pub id: EntityId,
24    pub components: Vec<ComponentSnapshot>,
25}
26
27/// A component snapshot.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ComponentSnapshot {
30    pub id: ComponentId,
31    /// Field values in schema order.
32    pub fields: Vec<FieldValue>,
33}
34
35/// A field value in decoded form.
36#[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
46/// Encodes a full snapshot into the provided output buffer.
47///
48/// Entities must be in deterministic order (ascending `EntityId` recommended).
49pub 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
95/// Decodes a full snapshot from raw packet bytes.
96pub 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
106/// Decodes a full snapshot from a parsed wire packet.
107pub 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        // Add a trailing padding byte to the section body and patch lengths.
866        let mut extra = buf[..bytes].to_vec();
867        extra[wire::HEADER_SIZE + 1] = 7; // section length varint
868        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(&section_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        // Flip the field mask bit off (component mask stays on).
1005        let payload_start = wire::HEADER_SIZE;
1006        let mask_offset = payload_start + 2 + 1 + 4; // tag + len + count + entity_id
1007        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}