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
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        // Add a trailing padding byte to the section body and patch lengths.
989        let mut extra = buf[..bytes].to_vec();
990        extra[wire::HEADER_SIZE + 1] = 7; // section length varint
991        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(&section_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        // Flip the field mask bit off (component mask stays on).
1128        let payload_start = wire::HEADER_SIZE;
1129        let mask_offset = payload_start + 2 + 1 + 4; // tag + len + count + entity_id
1130        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}