ifc_lite_geometry/instancing/
wire_decode.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! The IFNS decoders, split from `wire.rs` (module-size ratchet) when #5984
6//! appended trailing field 2. `wire.rs` stays the encoder and the layout's
7//! constants; the format itself is documented in `instancing/mod.rs`.
8
9use super::wire::{
10    DecodedInstance, DecodedInstanced, DecodedTemplate, HEADER_BYTES, INSTANCED_MAGIC,
11    INSTANCE_FINISH_OFFSET, INSTANCE_ITEM_ID_OFFSET, INSTANCE_RECORD_BASE_BYTES,
12    INSTANCE_RECORD_ITEM_ID_BYTES, TEMPLATE_RECORD_BYTES,
13};
14
15/// Decode an instanced shard. Returns None on a bad magic/version or truncation.
16pub fn decode_instanced(bytes: &[u8]) -> Option<DecodedInstanced> {
17    let ru32 = |o: usize| -> Option<u32> {
18        bytes.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap()))
19    };
20    let rf32 = |o: usize| -> Option<f32> {
21        bytes.get(o..o + 4).map(|s| f32::from_le_bytes(s.try_into().unwrap()))
22    };
23    let rf64 = |o: usize| -> Option<f64> {
24        bytes.get(o..o + 8).map(|s| f64::from_le_bytes(s.try_into().unwrap()))
25    };
26    // PERMISSIVE on version, STRICT on stride. Rejecting a version above this
27    // build's would reject exactly the shards forward compatibility is for: a
28    // v3 that APPENDS a trailing field is still fully readable here, because
29    // every field this build knows sits at a fixed offset inside the base
30    // record and the declared stride steps over the tail it does not know.
31    // Refusing it would also have rejected the v1 shards already sitting in
32    // browser caches, which persist IFNS bytes verbatim rather than re-encoding
33    // — a silent loss of all instanced geometry on every existing entry.
34    // Version 0 is not a version.
35    let version = ru32(4)?;
36    if ru32(0)? != INSTANCED_MAGIC || version == 0 {
37        return None;
38    }
39    let template_count = ru32(8)? as usize;
40    let instance_count = ru32(12)? as usize;
41    let positions_len = ru32(16)? as usize;
42    let normals_len = ru32(20)? as usize;
43    let _indices_len = ru32(24)? as usize;
44    // Word 7 is `reserved` in v1 and the instance record STRIDE from v2 on. v1
45    // wrote a literal 0 there, which is not a legal stride, so both readings of
46    // a v1 shard land on the 88-byte base record.
47    let declared_stride = if version >= 2 { ru32(28)? as usize } else { 0 };
48    let inst_bytes = if declared_stride == 0 {
49        INSTANCE_RECORD_BASE_BYTES
50    } else {
51        declared_stride
52    };
53    // A stride below the base is not a shorter record, it is a corrupt header:
54    // the base fields are not optional. Reading at it would slice each record
55    // out of its predecessor's transform and yield plausible garbage. An
56    // UNALIGNED stride is refused beside it, in BOTH languages: every field on
57    // this wire is 4 bytes, so a boundary off a 4-byte multiple names no field,
58    // and the TS decoder cannot even attempt one — it views the data pools as
59    // `Float32Array` over the shard buffer, which throws an opaque `RangeError`
60    // where this decoder used to read the same bytes happily.
61    if inst_bytes < INSTANCE_RECORD_BASE_BYTES || inst_bytes % 4 != 0 {
62        return None;
63    }
64
65    // Checked throughout: `instance_count` and the stride are both attacker-
66    // controlled u32s, so their product overflows a 32-bit usize (wasm32) and
67    // can reach 2^64 on a 64-bit host. An overflow here would wrap the data
68    // offset back INSIDE the buffer and every bounds check below would pass.
69    let tt_off = HEADER_BYTES;
70    let it_off = tt_off.checked_add(template_count.checked_mul(TEMPLATE_RECORD_BYTES)?)?;
71    let data_off = it_off.checked_add(instance_count.checked_mul(inst_bytes)?)?;
72    let nrm_data = data_off.checked_add(positions_len.checked_mul(4)?)?;
73    let idx_data = nrm_data.checked_add(normals_len.checked_mul(4)?)?;
74
75    // A corrupt/hostile header can claim an arbitrary template_count or
76    // instance_count. Bound both against the buffer we actually have BEFORE
77    // sizing `Vec::with_capacity` below — otherwise a bogus huge count tries
78    // to reserve gigabytes (or aborts the process via the allocator's OOM
79    // handler) long before the per-field `ru32`/`rf32` reads below would ever
80    // get a chance to fail gracefully and return `None`. This is also what
81    // validates the declared stride against the buffer: a stride that does not
82    // fit the instance table it describes cannot reach the data pools.
83    if bytes.len() < data_off {
84        return None;
85    }
86
87    // Byte offset of element `k` of the pool at `base` whose template range
88    // starts at element `off` — checked against the same attacker-controlled
89    // u32s, and what makes "Checked throughout" above true of the pool reads
90    // too. On wasm32 (usize = 32 bits) `pos_off = 0xFFFFFFFF` overflows
91    // `base + (off + k) * 4`: debug traps rather than returning the promised
92    // `None`, release wraps back INSIDE the buffer and returns WRONG geometry.
93    let elem = |base: usize, off: usize, k: usize| -> Option<usize> {
94        base.checked_add(off.checked_add(k)?.checked_mul(4)?)
95    };
96
97    let mut templates = Vec::with_capacity(template_count);
98    for t in 0..template_count {
99        let r = tt_off + t * TEMPLATE_RECORD_BYTES;
100        let pos_off = ru32(r)? as usize;
101        let pos_len = ru32(r + 4)? as usize;
102        let nrm_off = ru32(r + 8)? as usize;
103        let nrm_len = ru32(r + 12)? as usize;
104        let i_off = ru32(r + 16)? as usize;
105        let i_len = ru32(r + 20)? as usize;
106        let origin = [rf64(r + 24)?, rf64(r + 32)?, rf64(r + 40)?];
107        let positions = (0..pos_len)
108            .map(|k| rf32(elem(data_off, pos_off, k)?))
109            .collect::<Option<Vec<f32>>>()?;
110        let normals = (0..nrm_len)
111            .map(|k| rf32(elem(nrm_data, nrm_off, k)?))
112            .collect::<Option<Vec<f32>>>()?;
113        let indices = (0..i_len)
114            .map(|k| ru32(elem(idx_data, i_off, k)?))
115            .collect::<Option<Vec<u32>>>()?;
116        templates.push(DecodedTemplate { positions, normals, indices, origin });
117    }
118
119    let mut instances = Vec::with_capacity(instance_count);
120    for i in 0..instance_count {
121        let r = it_off + i * inst_bytes;
122        let template_index = ru32(r)?;
123        let entity_id = ru32(r + 4)?;
124        let mut color = [0.0f32; 4];
125        for (k, c) in color.iter_mut().enumerate() {
126            *c = rf32(r + 8 + k * 4)?;
127        }
128        let mut transform = [0.0f32; 16];
129        for (k, v) in transform.iter_mut().enumerate() {
130            *v = rf32(r + 24 + k * 4)?;
131        }
132        // Trailing field 1, present only when the stride makes room for it.
133        // Anything the stride reaches BEYOND it is a field appended by a newer
134        // producer: skipped, not an error — that is the forward compatibility
135        // the stride buys. 0 is the producer's "no item" sentinel (STEP names
136        // start at #1), and a shard without the field has none at all — both
137        // surface as None, so a consumer cannot tell an absent id apart from a
138        // fabricated #0.
139        let item_id = if inst_bytes >= INSTANCE_RECORD_ITEM_ID_BYTES {
140            Some(ru32(r + INSTANCE_ITEM_ID_OFFSET)?).filter(|&id| id != 0)
141        } else {
142            None
143        };
144        instances.push(DecodedInstance { template_index, entity_id, color, transform, item_id });
145    }
146    Some(DecodedInstanced { templates, instances })
147}
148
149
150/// Trailing field 2 of every instance record, in instance order (#5984):
151/// `[metallic, roughness]`, NaN where unauthored. A shard whose stride does
152/// not reach the field (v1, v2, or a v3 written by a model without finishes)
153/// reads as all-NaN rather than failing, the same forward/backward rule the
154/// item id follows. `None` exactly when [`decode_instanced`] would refuse the
155/// header or the instance table does not fit the buffer.
156pub fn decode_instance_finishes(bytes: &[u8]) -> Option<Vec<[f32; 2]>> {
157    let ru32 = |o: usize| bytes.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap()));
158    let rf32 = |o: usize| bytes.get(o..o + 4).map(|s| f32::from_le_bytes(s.try_into().unwrap()));
159    let version = ru32(4)?;
160    if ru32(0)? != INSTANCED_MAGIC || version == 0 {
161        return None;
162    }
163    let template_count = ru32(8)? as usize;
164    let instance_count = ru32(12)? as usize;
165    let declared = if version >= 2 { ru32(28)? as usize } else { 0 };
166    let stride = if declared == 0 { INSTANCE_RECORD_BASE_BYTES } else { declared };
167    if stride < INSTANCE_RECORD_BASE_BYTES || stride % 4 != 0 {
168        return None;
169    }
170    let it_off = HEADER_BYTES.checked_add(template_count.checked_mul(TEMPLATE_RECORD_BYTES)?)?;
171    if bytes.len() < it_off.checked_add(instance_count.checked_mul(stride)?)? {
172        return None;
173    }
174    let carries = stride >= INSTANCE_FINISH_OFFSET + 8;
175    (0..instance_count)
176        .map(|i| {
177            let r = it_off + i * stride + INSTANCE_FINISH_OFFSET;
178            Some(if carries { [rf32(r)?, rf32(r + 4)?] } else { [f32::NAN; 2] })
179        })
180        .collect()
181}
182
183#[cfg(test)]
184#[path = "wire_finish_tests.rs"]
185mod finish_tests;