use crate::status::{DracoError, Status};
pub(crate) const MAX_ALLOCATED_BYTES_PER_INPUT_BYTE: usize = 1 << 20;
pub(crate) const MAX_UNBACKED_BYTES: usize = 256 << 20;
pub(crate) fn ensure_allocation_is_backed(requested_bytes: usize, stream_bytes: usize) -> Status {
if requested_bytes > MAX_UNBACKED_BYTES
|| requested_bytes / MAX_ALLOCATED_BYTES_PER_INPUT_BYTE > stream_bytes
{
return Err(DracoError::allocation_exceeds_input(
requested_bytes,
stream_bytes,
));
}
Ok(())
}
pub(crate) fn ensure_symbols_are_backed(count: usize, stream_bytes: usize) -> Status {
if count > stream_bytes.saturating_mul(8) {
return Err(DracoError::new(
crate::status::ErrorKind::AllocationExceedsInput,
format!(
"declared {count} symbols, more than the {stream_bytes} byte stream \
could carry at one bit each"
),
));
}
Ok(())
}
#[cfg(all(test, feature = "decoder"))]
mod corpus {
#[test]
fn no_tracked_fixture_spends_the_budget() {
use crate::decoder_buffer::DecoderBuffer;
use std::path::PathBuf;
fn walk(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
for entry in std::fs::read_dir(dir).into_iter().flatten().flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|e| e == "drc") {
out.push(path);
}
}
}
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../testdata");
let mut files = Vec::new();
walk(&root, &mut files);
files.sort();
assert!(
files.len() > 50,
"found only {} .drc fixtures: the walk is wrong, not the corpus",
files.len()
);
let mut decoded_ok = 0;
for path in &files {
let bytes = std::fs::read(path).expect("read fixture");
if bytes.is_empty() {
continue;
}
for as_mesh in [true, false] {
let mut buffer = DecoderBuffer::new(&bytes);
let ok = if as_mesh {
let mut mesh = crate::mesh::Mesh::new();
crate::mesh_decoder::MeshDecoder::new()
.decode(&mut buffer, &mut mesh)
.is_ok()
} else {
let mut point_cloud = crate::point_cloud::PointCloud::new();
crate::point_cloud_decoder::PointCloudDecoder::new()
.decode(&mut buffer, &mut point_cloud)
.is_ok()
};
if !ok {
continue;
}
decoded_ok += 1;
const ROUNDING_ERROR: usize = super::MAX_UNBACKED_BYTES / 1024;
assert!(
buffer.spent() < ROUNDING_ERROR,
"{} charged {} bytes against the budget, past the {ROUNDING_ERROR} a constant attribute can explain",
path.display(),
buffer.spent()
);
}
}
assert!(decoded_ok > 50, "only {decoded_ok} fixtures decoded");
}
#[test]
#[cfg(feature = "encoder")]
fn a_stream_that_decodes_far_larger_than_itself_is_not_refused() {
use crate::decoder_buffer::DecoderBuffer;
use crate::draco_types::DataType;
use crate::encoder_buffer::EncoderBuffer;
use crate::encoder_options::EncoderOptions;
use crate::geometry_attribute::{GeometryAttributeType, PointAttribute};
use crate::point_cloud_encoder::PointCloudEncoder;
const NUM_POINTS: usize = 6_000_000;
let mut point_cloud = crate::point_cloud::PointCloud::new();
point_cloud.set_num_points(NUM_POINTS);
let mut position = PointAttribute::new();
position.init(
GeometryAttributeType::Position,
3,
DataType::Float32,
false,
NUM_POINTS,
);
point_cloud.add_attribute(position);
let mut options = EncoderOptions::new();
options.set_attribute_int(0, "quantization_bits", 8);
let mut encoded = EncoderBuffer::new();
let mut encoder = PointCloudEncoder::new();
encoder.set_point_cloud(point_cloud);
encoder.encode(&options, &mut encoded).expect("encode");
let stream = encoded.data().to_vec();
assert!(
stream.len() < 1024,
"the point of this test is a stream far smaller than what it decodes to, got {}",
stream.len()
);
let mut buffer = DecoderBuffer::new(&stream);
let mut decoded = crate::point_cloud::PointCloud::new();
crate::point_cloud_decoder::PointCloudDecoder::new()
.decode(&mut buffer, &mut decoded)
.expect("a stream this crate wrote must decode back");
assert_eq!(decoded.num_points(), NUM_POINTS);
assert_eq!(buffer.spent(), 0, "a legitimate file spent the backstop");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_stream_backs_an_allocation_within_the_ratio() {
assert!(ensure_allocation_is_backed(MAX_ALLOCATED_BYTES_PER_INPUT_BYTE, 1).is_ok());
assert!(ensure_allocation_is_backed(100_000 * 12, 171).is_ok());
}
#[test]
fn a_long_stream_does_not_buy_an_unbounded_reservation() {
assert!(ensure_allocation_is_backed(MAX_UNBACKED_BYTES, usize::MAX).is_ok());
assert!(ensure_allocation_is_backed(MAX_UNBACKED_BYTES + 1, usize::MAX).is_err());
assert!(ensure_allocation_is_backed(21_219_601_020, 27_911).is_err());
}
#[test]
fn a_tiny_stream_does_not_back_a_huge_allocation() {
assert!(ensure_allocation_is_backed(u32::MAX as usize * 12, 12).is_err());
}
#[test]
fn a_symbol_count_beyond_one_bit_each_is_refused() {
assert!(ensure_symbols_are_backed(3_287_731_392, 26_386).is_err());
assert!(ensure_allocation_is_backed(3_287_731_392 * 4, usize::MAX).is_err());
}
#[test]
fn a_symbol_count_of_exactly_one_bit_each_is_accepted() {
assert!(ensure_symbols_are_backed(8, 1).is_ok());
assert!(ensure_symbols_are_backed(9, 1).is_err());
}
#[test]
fn the_bound_survives_a_32_bit_usize() {
assert!(ensure_allocation_is_backed(usize::MAX, 4096).is_err());
}
}