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: every count a stream declares is admitted, so a
84    /// decode is bounded only by what the machine will allocate.
85    ///
86    /// For a caller that decodes trusted local input and would rather have a
87    /// scan loaded whole than a refusal.
88    pub fn permissive() -> Self {
89        Self {
90            max_points: u64::MAX,
91            max_faces: u64::MAX,
92            max_decoded_bytes: u64::MAX,
93        }
94    }
95
96    /// Tight ceilings for fuzzing, so a reported allocation failure is a real
97    /// bug rather than the fuzzer feeding a legitimately huge count.
98    pub fn fuzzing() -> Self {
99        Self {
100            max_points: 1 << 20,
101            max_faces: 1 << 20,
102            max_decoded_bytes: 64 << 20,
103        }
104    }
105
106    // Only the point-cloud header carries a point count, and that path is
107    // gated: a `decoder` build without `point_cloud_decode` never asks.
108    #[cfg(feature = "point_cloud_decode")]
109    pub(crate) fn check_points(self, points: u64) -> Status {
110        Self::check("points", points, self.max_points)
111    }
112
113    #[cfg(feature = "decoder")]
114    pub(crate) fn check_faces(self, faces: u64) -> Status {
115        Self::check("faces", faces, self.max_faces)
116    }
117
118    #[cfg(feature = "decoder")]
119    pub(crate) fn check_decoded_bytes(self, bytes: u64) -> Status {
120        Self::check("decoded attribute bytes", bytes, self.max_decoded_bytes)
121    }
122
123    #[cfg(feature = "decoder")]
124    fn check(what: &str, value: u64, ceiling: u64) -> Status {
125        if value > ceiling {
126            return Err(DracoError::new(
127                ErrorKind::LimitExceeded,
128                format!("stream decodes {value} {what}, over the caller's ceiling of {ceiling}"),
129            ));
130        }
131        Ok(())
132    }
133}
134
135// `#[non_exhaustive]` blocks `..Default::default()` for downstream crates, so
136// the type would be unconfigurable outside this crate without these setters.
137macro_rules! limit_setter {
138    ($name:ident, $field:ident, $doc:expr) => {
139        #[doc = $doc]
140        #[must_use]
141        pub fn $name(mut self, value: u64) -> Self {
142            self.$field = value;
143            self
144        }
145    };
146}
147
148impl DecodeLimits {
149    limit_setter!(with_max_points, max_points, "Sets [`Self::max_points`].");
150    limit_setter!(with_max_faces, max_faces, "Sets [`Self::max_faces`].");
151    limit_setter!(
152        with_max_decoded_bytes,
153        max_decoded_bytes,
154        "Sets [`Self::max_decoded_bytes`]."
155    );
156}
157
158#[cfg(all(test, feature = "point_cloud_decode"))]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn the_defaults_clear_every_asset_the_calibration_measured() {
164        let limits = DecodeLimits::default();
165        // The largest single stream of each kind measured against real assets.
166        assert!(limits.check_points(25_000_000).is_ok());
167        assert!(limits.check_faces(6_618_864).is_ok());
168        assert!(limits.check_decoded_bytes(700_000_000).is_ok());
169    }
170
171    #[test]
172    fn the_defaults_refuse_the_claim_that_motivated_them() {
173        let limits = DecodeLimits::default();
174        // The `decode_drc` artifact's header, and what it would occupy as
175        // three-component `f32` positions.
176        assert!(limits.check_points(1_073_741_828).is_err());
177        assert!(limits.check_decoded_bytes(1_073_741_828 * 12).is_err());
178    }
179
180    #[test]
181    fn permissive_limits_refuse_nothing() {
182        let limits = DecodeLimits::permissive();
183        assert!(limits.check_points(u64::MAX).is_ok());
184        assert!(limits.check_faces(u64::MAX).is_ok());
185        assert!(limits.check_decoded_bytes(u64::MAX).is_ok());
186    }
187
188    #[test]
189    fn a_refusal_names_the_caller_rather_than_the_file() {
190        let error = DecodeLimits::default()
191            .check_points(u64::MAX)
192            .expect_err("over the ceiling");
193        assert_eq!(error.kind(), ErrorKind::LimitExceeded);
194        assert!(error.message().contains("ceiling"), "{error}");
195    }
196}
197
198/// End to end: the ceilings refuse a stream this crate wrote, and the defaults
199/// do not.
200///
201/// The arithmetic tests above pin the comparison; these pin the wiring, which
202/// is the part that can silently stop existing.
203#[cfg(all(test, feature = "encoder", feature = "point_cloud_decode"))]
204mod wiring {
205    use super::*;
206    use crate::decoder_buffer::DecoderBuffer;
207    use crate::draco_types::DataType;
208    use crate::encoder_buffer::EncoderBuffer;
209    use crate::encoder_options::EncoderOptions;
210    use crate::geometry_attribute::{GeometryAttributeType, PointAttribute};
211    use crate::point_cloud::PointCloud;
212    use crate::point_cloud_decoder::PointCloudDecoder;
213    use crate::point_cloud_encoder::PointCloudEncoder;
214
215    const NUM_POINTS: usize = 100_000;
216
217    fn a_stream_of_100k_points() -> Vec<u8> {
218        let mut point_cloud = PointCloud::new();
219        point_cloud.set_num_points(NUM_POINTS);
220        let mut position = PointAttribute::new();
221        position.init(
222            GeometryAttributeType::Position,
223            3,
224            DataType::Float32,
225            false,
226            NUM_POINTS,
227        );
228        point_cloud.add_attribute(position);
229
230        let mut options = EncoderOptions::new();
231        options.set_attribute_int(0, "quantization_bits", 8);
232        let mut encoded = EncoderBuffer::new();
233        let mut encoder = PointCloudEncoder::new();
234        encoder.set_point_cloud(point_cloud);
235        encoder.encode(&options, &mut encoded).expect("encode");
236        encoded.data().to_vec()
237    }
238
239    fn decode_under(limits: DecodeLimits, stream: &[u8]) -> Result<usize, DracoError> {
240        let mut buffer = DecoderBuffer::new(stream).with_limits(limits);
241        let mut decoded = PointCloud::new();
242        PointCloudDecoder::new().decode(&mut buffer, &mut decoded)?;
243        Ok(decoded.num_points())
244    }
245
246    #[test]
247    fn the_defaults_decode_a_stream_this_crate_wrote() {
248        let stream = a_stream_of_100k_points();
249        assert_eq!(
250            decode_under(DecodeLimits::default(), &stream).expect("within the defaults"),
251            NUM_POINTS
252        );
253    }
254
255    #[test]
256    fn a_point_ceiling_below_the_header_refuses_it() {
257        let stream = a_stream_of_100k_points();
258        let error = decode_under(DecodeLimits::default().with_max_points(1_000), &stream)
259            .expect_err("over the ceiling");
260        assert_eq!(error.kind(), ErrorKind::LimitExceeded);
261        assert!(error.message().contains("points"), "{error}");
262    }
263
264    /// The byte ceiling is the one that bounds memory, and it is charged from
265    /// the attribute layout rather than from the point count: three `f32`
266    /// components over 100,000 points is 1.2 MB whatever the stream costs.
267    #[test]
268    fn a_byte_ceiling_below_the_attribute_layout_refuses_it() {
269        let stream = a_stream_of_100k_points();
270        let error = decode_under(
271            DecodeLimits::default().with_max_decoded_bytes(NUM_POINTS as u64 * 12 - 1),
272            &stream,
273        )
274        .expect_err("over the ceiling");
275        assert_eq!(error.kind(), ErrorKind::LimitExceeded, "{error}");
276        assert!(
277            error.message().contains("decoded attribute bytes"),
278            "{error}"
279        );
280
281        assert_eq!(
282            decode_under(
283                DecodeLimits::default().with_max_decoded_bytes(NUM_POINTS as u64 * 12),
284                &stream
285            )
286            .expect("exactly the layout fits"),
287            NUM_POINTS
288        );
289    }
290
291    /// The face ceiling sits on the connectivity, which neither of the other
292    /// two reaches: a mesh can be far more faces than points.
293    #[test]
294    fn a_face_ceiling_below_the_connectivity_refuses_it() {
295        use crate::mesh::Mesh;
296        use crate::mesh_decoder::MeshDecoder;
297        use crate::mesh_encoder::MeshEncoder;
298
299        const SIDE: u32 = 64;
300        let mut mesh = Mesh::new();
301        let points = (SIDE * SIDE) as usize;
302        mesh.set_num_points(points);
303        let mut position = PointAttribute::new();
304        position.init(
305            GeometryAttributeType::Position,
306            3,
307            DataType::Float32,
308            false,
309            points,
310        );
311        mesh.add_attribute(position);
312        let mut faces = Vec::new();
313        for y in 0..SIDE - 1 {
314            for x in 0..SIDE - 1 {
315                let a = y * SIDE + x;
316                faces.push([a, a + 1, a + SIDE]);
317                faces.push([a + 1, a + SIDE + 1, a + SIDE]);
318            }
319        }
320        mesh.try_set_num_faces(faces.len()).expect("faces");
321        for (index, face) in faces.iter().enumerate() {
322            mesh.set_face_from_indices(index, *face);
323        }
324        let num_faces = faces.len();
325
326        let mut encoded = EncoderBuffer::new();
327        let mut encoder = MeshEncoder::new();
328        encoder.set_mesh(mesh);
329        encoder
330            .encode(&EncoderOptions::new(), &mut encoded)
331            .expect("encode");
332        let stream = encoded.data().to_vec();
333
334        let decode = |limits: DecodeLimits| {
335            let mut buffer = DecoderBuffer::new(&stream).with_limits(limits);
336            let mut decoded = Mesh::new();
337            MeshDecoder::new()
338                .decode(&mut buffer, &mut decoded)
339                .map(|()| decoded.num_faces())
340        };
341
342        assert_eq!(
343            decode(DecodeLimits::default()).expect("within the defaults"),
344            num_faces
345        );
346        let error = decode(DecodeLimits::default().with_max_faces(num_faces as u64 - 1))
347            .expect_err("over the ceiling");
348        assert_eq!(error.kind(), ErrorKind::LimitExceeded, "{error}");
349        assert!(error.message().contains("faces"), "{error}");
350    }
351
352    #[test]
353    fn permissive_limits_decode_what_the_defaults_do() {
354        let stream = a_stream_of_100k_points();
355        assert_eq!(
356            decode_under(DecodeLimits::permissive(), &stream).expect("no ceiling"),
357            NUM_POINTS
358        );
359    }
360}