Skip to main content

draco_core/
decode_limits.rs

1//! Caller-set ceilings on what one decode may produce.
2//!
3//! `decode_budget` and this module answer different
4//! questions and both are needed. The budget is a backstop against a
5//! *reservation nothing backs* -- a header naming gigabytes over a stream that
6//! carries none of them -- and it is always on. These are the caller's policy
7//! on *how large a decode may legitimately be*, and they count what the file
8//! honestly describes as well as what it lies about.
9//!
10//! The reason the budget cannot do this job is measured: legitimate geometry
11//! reaches four to five orders of magnitude more output than input, because a
12//! constant attribute entropy-codes to a size independent of its count. A
13//! stream of under a kilobyte decodes six million points, and refusing it
14//! would be the interoperability bug `decode_budget` exists to have removed.
15//! No ratio separates that from a hostile claim, so the only honest instrument
16//! is an absolute ceiling, and only the caller knows where it sits.
17//!
18//! `SECURITY.md` records that this decoder does not cap reconstructed geometry
19//! by design. That stays true of the format; what changes is that the caller
20//! can now say otherwise, and the default says it for them.
21
22#[cfg(feature = "decoder")]
23use crate::status::{DracoError, ErrorKind, Status};
24
25/// Ceilings on one decode, applied to what the stream reconstructs.
26///
27/// Every field is a hard ceiling: exceeding one fails the decode with
28/// [`ErrorKind::LimitExceeded`], which a caller can tell apart from
29/// [`ErrorKind::AllocationExceedsInput`] -- its own policy refusing a large
30/// file, rather than the decoder refusing a malformed one.
31///
32/// The counts are the *decoded* ones, not the ones a source file was authored
33/// with. Draco splits a point wherever an attribute seam runs through it, and
34/// the growth is real: measured across ten assets it runs from zero to 7.7%,
35/// and one exporter's 65,532-point primitive decodes to 76,742. A ceiling
36/// derived from what an exporter writes would refuse files that follow its own
37/// convention.
38///
39/// Install with [`DecoderBuffer::with_limits`](crate::decoder_buffer::DecoderBuffer::with_limits).
40///
41/// ```
42/// use draco_core::{DecodeLimits, DecoderBuffer};
43///
44/// let stream = [0u8; 0];
45/// let buffer = DecoderBuffer::new(&stream)
46///     .with_limits(DecodeLimits::default().with_max_points(1_000_000));
47/// # let _ = buffer;
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct DecodeLimits {
52    /// Largest accepted decoded point count.
53    pub max_points: u64,
54    /// Largest accepted decoded face count.
55    pub max_faces: u64,
56    /// Largest accepted total size of decoded attribute values, in bytes.
57    ///
58    /// This is the one that bounds memory. The other two are proxies whose
59    /// relationship to bytes the attribute set moves around: of two measured
60    /// assets, the one with a third fewer points held twice the bytes per
61    /// primitive, because it carried tangents and five texture-coordinate sets
62    /// against a position and a normal.
63    pub max_decoded_bytes: u64,
64}
65
66impl Default for DecodeLimits {
67    fn default() -> Self {
68        // Largest seen in one decoded stream across a corpus of real assets:
69        // 25,000,000 points and 700 MB (a photogrammetric point cloud) and
70        // 6,618,864 faces (an unchunked OBJ). These sit an order of magnitude
71        // above that, because their job is to stop an absurd header rather
72        // than to police legitimate files -- a caller with a real memory
73        // budget sets its own.
74        Self {
75            max_points: 256_000_000,
76            max_faces: 512_000_000,
77            max_decoded_bytes: 2 << 30,
78        }
79    }
80}
81
82impl DecodeLimits {
83    /// No ceiling at all, which is what this crate did before the type existed.
84    ///
85    /// For a caller that decodes trusted local input and would rather have a
86    /// scan loaded whole than a refusal.
87    pub fn permissive() -> Self {
88        Self {
89            max_points: u64::MAX,
90            max_faces: u64::MAX,
91            max_decoded_bytes: u64::MAX,
92        }
93    }
94
95    /// Tight ceilings for fuzzing, so a reported allocation failure is a real
96    /// bug rather than the fuzzer feeding a legitimately huge count.
97    pub fn fuzzing() -> Self {
98        Self {
99            max_points: 1 << 20,
100            max_faces: 1 << 20,
101            max_decoded_bytes: 64 << 20,
102        }
103    }
104
105    // Only the point-cloud header carries a point count, and that path is
106    // gated: a `decoder` build without `point_cloud_decode` never asks.
107    #[cfg(feature = "point_cloud_decode")]
108    pub(crate) fn check_points(self, points: u64) -> Status {
109        Self::check("points", points, self.max_points)
110    }
111
112    #[cfg(feature = "decoder")]
113    pub(crate) fn check_faces(self, faces: u64) -> Status {
114        Self::check("faces", faces, self.max_faces)
115    }
116
117    #[cfg(feature = "decoder")]
118    pub(crate) fn check_decoded_bytes(self, bytes: u64) -> Status {
119        Self::check("decoded attribute bytes", bytes, self.max_decoded_bytes)
120    }
121
122    #[cfg(feature = "decoder")]
123    fn check(what: &str, value: u64, ceiling: u64) -> Status {
124        if value > ceiling {
125            return Err(DracoError::new(
126                ErrorKind::LimitExceeded,
127                format!("stream decodes {value} {what}, over the caller's ceiling of {ceiling}"),
128            ));
129        }
130        Ok(())
131    }
132}
133
134// `#[non_exhaustive]` blocks `..Default::default()` for downstream crates, so
135// the type would be unconfigurable outside this crate without these setters.
136macro_rules! limit_setter {
137    ($name:ident, $field:ident, $doc:expr) => {
138        #[doc = $doc]
139        #[must_use]
140        pub fn $name(mut self, value: u64) -> Self {
141            self.$field = value;
142            self
143        }
144    };
145}
146
147impl DecodeLimits {
148    limit_setter!(with_max_points, max_points, "Sets [`Self::max_points`].");
149    limit_setter!(with_max_faces, max_faces, "Sets [`Self::max_faces`].");
150    limit_setter!(
151        with_max_decoded_bytes,
152        max_decoded_bytes,
153        "Sets [`Self::max_decoded_bytes`]."
154    );
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn the_defaults_clear_every_asset_the_calibration_measured() {
163        let limits = DecodeLimits::default();
164        // The largest single stream of each kind measured against real assets.
165        assert!(limits.check_points(25_000_000).is_ok());
166        assert!(limits.check_faces(6_618_864).is_ok());
167        assert!(limits.check_decoded_bytes(700_000_000).is_ok());
168    }
169
170    #[test]
171    fn the_defaults_refuse_the_claim_that_motivated_them() {
172        let limits = DecodeLimits::default();
173        // The `decode_drc` artifact's header, and what it would occupy as
174        // three-component `f32` positions.
175        assert!(limits.check_points(1_073_741_828).is_err());
176        assert!(limits.check_decoded_bytes(1_073_741_828 * 12).is_err());
177    }
178
179    #[test]
180    fn permissive_limits_refuse_nothing() {
181        let limits = DecodeLimits::permissive();
182        assert!(limits.check_points(u64::MAX).is_ok());
183        assert!(limits.check_faces(u64::MAX).is_ok());
184        assert!(limits.check_decoded_bytes(u64::MAX).is_ok());
185    }
186
187    #[test]
188    fn a_refusal_names_the_caller_rather_than_the_file() {
189        let error = DecodeLimits::default()
190            .check_points(u64::MAX)
191            .expect_err("over the ceiling");
192        assert_eq!(error.kind(), ErrorKind::LimitExceeded);
193        assert!(error.message().contains("ceiling"), "{error}");
194    }
195}
196
197/// End to end: the ceilings refuse a stream this crate wrote, and the defaults
198/// do not.
199///
200/// The arithmetic tests above pin the comparison; these pin the wiring, which
201/// is the part that can silently stop existing.
202#[cfg(all(test, feature = "encoder", feature = "decoder"))]
203mod wiring {
204    use super::*;
205    use crate::decoder_buffer::DecoderBuffer;
206    use crate::draco_types::DataType;
207    use crate::encoder_buffer::EncoderBuffer;
208    use crate::encoder_options::EncoderOptions;
209    use crate::geometry_attribute::{GeometryAttributeType, PointAttribute};
210    use crate::point_cloud::PointCloud;
211    use crate::point_cloud_decoder::PointCloudDecoder;
212    use crate::point_cloud_encoder::PointCloudEncoder;
213
214    const NUM_POINTS: usize = 100_000;
215
216    fn a_stream_of_100k_points() -> Vec<u8> {
217        let mut point_cloud = PointCloud::new();
218        point_cloud.set_num_points(NUM_POINTS);
219        let mut position = PointAttribute::new();
220        position.init(
221            GeometryAttributeType::Position,
222            3,
223            DataType::Float32,
224            false,
225            NUM_POINTS,
226        );
227        point_cloud.add_attribute(position);
228
229        let mut options = EncoderOptions::new();
230        options.set_attribute_int(0, "quantization_bits", 8);
231        let mut encoded = EncoderBuffer::new();
232        let mut encoder = PointCloudEncoder::new();
233        encoder.set_point_cloud(point_cloud);
234        encoder.encode(&options, &mut encoded).expect("encode");
235        encoded.data().to_vec()
236    }
237
238    fn decode_under(limits: DecodeLimits, stream: &[u8]) -> Result<usize, DracoError> {
239        let mut buffer = DecoderBuffer::new(stream).with_limits(limits);
240        let mut decoded = PointCloud::new();
241        PointCloudDecoder::new().decode(&mut buffer, &mut decoded)?;
242        Ok(decoded.num_points())
243    }
244
245    #[test]
246    fn the_defaults_decode_a_stream_this_crate_wrote() {
247        let stream = a_stream_of_100k_points();
248        assert_eq!(
249            decode_under(DecodeLimits::default(), &stream).expect("within the defaults"),
250            NUM_POINTS
251        );
252    }
253
254    #[test]
255    fn a_point_ceiling_below_the_header_refuses_it() {
256        let stream = a_stream_of_100k_points();
257        let error = decode_under(DecodeLimits::default().with_max_points(1_000), &stream)
258            .expect_err("over the ceiling");
259        assert_eq!(error.kind(), ErrorKind::LimitExceeded);
260        assert!(error.message().contains("points"), "{error}");
261    }
262
263    /// The byte ceiling is the one that bounds memory, and it is charged from
264    /// the attribute layout rather than from the point count: three `f32`
265    /// components over 100,000 points is 1.2 MB whatever the stream costs.
266    #[test]
267    fn a_byte_ceiling_below_the_attribute_layout_refuses_it() {
268        let stream = a_stream_of_100k_points();
269        let error = decode_under(
270            DecodeLimits::default().with_max_decoded_bytes(NUM_POINTS as u64 * 12 - 1),
271            &stream,
272        )
273        .expect_err("over the ceiling");
274        assert_eq!(error.kind(), ErrorKind::LimitExceeded, "{error}");
275        assert!(
276            error.message().contains("decoded attribute bytes"),
277            "{error}"
278        );
279
280        assert_eq!(
281            decode_under(
282                DecodeLimits::default().with_max_decoded_bytes(NUM_POINTS as u64 * 12),
283                &stream
284            )
285            .expect("exactly the layout fits"),
286            NUM_POINTS
287        );
288    }
289
290    /// The face ceiling sits on the connectivity, which neither of the other
291    /// two reaches: a mesh can be far more faces than points.
292    #[test]
293    fn a_face_ceiling_below_the_connectivity_refuses_it() {
294        use crate::mesh::Mesh;
295        use crate::mesh_decoder::MeshDecoder;
296        use crate::mesh_encoder::MeshEncoder;
297
298        const SIDE: u32 = 64;
299        let mut mesh = Mesh::new();
300        let points = (SIDE * SIDE) as usize;
301        mesh.set_num_points(points);
302        let mut position = PointAttribute::new();
303        position.init(
304            GeometryAttributeType::Position,
305            3,
306            DataType::Float32,
307            false,
308            points,
309        );
310        mesh.add_attribute(position);
311        let mut faces = Vec::new();
312        for y in 0..SIDE - 1 {
313            for x in 0..SIDE - 1 {
314                let a = y * SIDE + x;
315                faces.push([a, a + 1, a + SIDE]);
316                faces.push([a + 1, a + SIDE + 1, a + SIDE]);
317            }
318        }
319        mesh.try_set_num_faces(faces.len()).expect("faces");
320        for (index, face) in faces.iter().enumerate() {
321            mesh.set_face_from_indices(index, *face);
322        }
323        let num_faces = faces.len();
324
325        let mut encoded = EncoderBuffer::new();
326        let mut encoder = MeshEncoder::new();
327        encoder.set_mesh(mesh);
328        encoder
329            .encode(&EncoderOptions::new(), &mut encoded)
330            .expect("encode");
331        let stream = encoded.data().to_vec();
332
333        let decode = |limits: DecodeLimits| {
334            let mut buffer = DecoderBuffer::new(&stream).with_limits(limits);
335            let mut decoded = Mesh::new();
336            MeshDecoder::new()
337                .decode(&mut buffer, &mut decoded)
338                .map(|()| decoded.num_faces())
339        };
340
341        assert_eq!(
342            decode(DecodeLimits::default()).expect("within the defaults"),
343            num_faces
344        );
345        let error = decode(DecodeLimits::default().with_max_faces(num_faces as u64 - 1))
346            .expect_err("over the ceiling");
347        assert_eq!(error.kind(), ErrorKind::LimitExceeded, "{error}");
348        assert!(error.message().contains("faces"), "{error}");
349    }
350
351    #[test]
352    fn permissive_limits_decode_what_the_defaults_do() {
353        let stream = a_stream_of_100k_points();
354        assert_eq!(
355            decode_under(DecodeLimits::permissive(), &stream).expect("no ceiling"),
356            NUM_POINTS
357        );
358    }
359}