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;