nanobrush / crates /mb3-dat /src /tests.rs
nmarshall's picture
formats: add safe dat codecs
918c06e
Raw History Blame Contribute Delete
20.8 kB
use super::*;
use std::io::{self, Cursor, Read, Write};
const VERSIONS: [Version; 2] = [Version::V201501280, Version::V201512200];
fn empty(version: Version) -> Document {
Document {
version,
world: World::default(),
}
}
fn rich(version: Version) -> Document {
let plane = Plane {
p0: [1.0, -0.0, 3.0],
u: [4.0, 5.0, 6.0],
v: [7.0, 8.0, 9.0],
polygon: Some(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]),
..Plane::default()
};
let mut d = Document {
version,
world: World {
computed: true,
brushes: vec![Brush {
planes: vec![plane],
}],
patches: vec![Patch9 {
anchors_u: 3,
anchors_v: 3,
quads_u: 2,
quads_v: 2,
anchors: vec![[1.0, 2.0, 3.0]; 9],
computed: Some(PatchComputed {
points: vec![[4.0, 5.0, 6.0]; 9],
normals: vec![[0.0, 1.0, 0.0]; 9],
}),
}],
..World::default()
},
};
if version == Version::V201512200 {
d.world.string_properties = vec!["unused".into(), "name".into()];
d.world.uv_properties = vec!["uv".into()];
d.world.brushes[0].planes[0].strings = vec![Property {
index: 1,
value: String::new(),
}];
d.world.brushes[0].planes[0].uv = vec![Property {
index: 0,
value: Uv {
u: [1.0, 2.0, 3.0],
v: [6.0, 7.0, 8.0],
u_scale: 4.0,
u_shift: 5.0,
v_scale: 9.0,
v_shift: 10.0,
},
}];
}
d
}
#[test]
fn exact_empty_headers() {
for version in VERSIONS {
let mut expected = b"Mb3World".to_vec();
expected.extend_from_slice(&(version as u32).to_le_bytes());
expected.extend_from_slice(&[0; 9]);
if version == Version::V201512200 {
expected.extend_from_slice(&[0; 2]);
}
let d = empty(version);
assert_eq!(encode(&d, &Limits::default()).unwrap(), expected);
assert_eq!(decode(&expected, &Limits::default()).unwrap(), d);
}
}
#[test]
fn rich_semantic_and_bit_exact_roundtrips() {
for version in VERSIONS {
let d = rich(version);
let data = encode(&d, &Limits::default()).unwrap();
let decoded = decode(&data, &Limits::default()).unwrap();
assert_eq!(decoded, d);
assert_eq!(
decoded.world.brushes[0].planes[0].p0[1].to_bits(),
(-0.0f32).to_bits()
);
assert_eq!(encode(&decoded, &Limits::default()).unwrap(), data);
}
}
#[test]
fn rejects_every_prefix_truncation() {
for version in VERSIONS {
let data = encode(&rich(version), &Limits::default()).unwrap();
for n in 0..data.len() {
assert!(
decode(&data[..n], &Limits::default()).is_err(),
"version {version:?}, prefix {n}"
);
}
}
}
#[test]
fn rejects_header_flags_versions_trailing_and_reserved_widths() {
for version in VERSIONS {
let data = encode(&empty(version), &Limits::default()).unwrap();
for (at, value) in [(0, b'x'), (8, 0), (12, 0xe0), (12, 6)] {
let mut bad = data.clone();
bad[at] = value;
assert!(decode(&bad, &Limits::default()).is_err());
}
let mut extra = data;
extra.push(0);
assert!(matches!(
decode(&extra, &Limits::default()).unwrap_err().kind,
ErrorKind::TrailingData
));
}
for flags in [3, 12, 0xf0] {
let mut data = encode(&empty(Version::V201512200), &Limits::default()).unwrap();
data[15..19].copy_from_slice(&1i32.to_le_bytes());
data.splice(19..19, [flags, 0]);
assert!(decode(&data, &Limits::default()).is_err());
}
}
#[test]
fn counts_rejected_before_allocation() {
for count in [-1i32, i32::MAX] {
let mut data = encode(&empty(Version::V201512200), &Limits::default()).unwrap();
data[15..19].copy_from_slice(&count.to_le_bytes());
assert!(decode(&data, &Limits::default()).is_err());
}
let mut old = encode(&empty(Version::V201501280), &Limits::default()).unwrap();
old[13..17].copy_from_slice(&1i32.to_le_bytes());
old.extend_from_slice(&[1, 0, 128]);
assert!(matches!(
decode(&old, &Limits::default()).unwrap_err().kind,
ErrorKind::NegativeCount
));
}
#[test]
fn plane_width_boundaries_and_legacy_repair() {
for version in VERSIONS {
for n in [255, 256, 32767, 32768, 65535, 65536] {
let mut d = empty(version);
d.world.brushes.push(Brush {
planes: vec![Plane::default(); n],
});
let data = encode(&d, &Limits::default()).unwrap();
let flag_at = if version == Version::V201501280 {
21
} else {
19
};
let expected = if n <= 255 {
0
} else if n
<= if version == Version::V201501280 {
32767
} else {
65535
}
{
1
} else {
2
};
assert_eq!(data[flag_at], expected);
assert_eq!(decode(&data, &Limits::default()).unwrap(), d);
}
}
}
#[test]
fn string_and_uv_table_width_boundaries_preserve_unused_names() {
for n in [255, 256, 65535, 65536] {
let mut d = empty(Version::V201512200);
d.world.string_properties = (0..n).map(|i| format!("s{i}")).collect();
d.world.uv_properties = (0..n).map(|i| format!("u{i}")).collect();
let data = encode(&d, &Limits::default()).unwrap();
let width = if n <= 255 {
0
} else if n <= 65535 {
1
} else {
2
};
assert_eq!(data[12], width << 1 | width << 3);
assert_eq!(decode(&data, &Limits::default()).unwrap(), d);
}
}
#[test]
fn sparse_index_and_sentinel_width_boundaries() {
for n in [255, 256, 65535, 65536] {
let mut d = empty(Version::V201512200);
d.world.string_properties = (0..n).map(|i| format!("p{i}")).collect();
d.world.uv_properties = d.world.string_properties.clone();
d.world.brushes.push(Brush {
planes: vec![Plane {
strings: vec![Property {
index: n - 1,
value: String::new(),
}],
uv: vec![Property {
index: n - 1,
value: Uv {
u: [1.0; 3],
v: [2.0; 3],
u_scale: 3.0,
u_shift: 4.0,
v_scale: 5.0,
v_shift: 6.0,
},
}],
..Plane::default()
}],
});
let data = encode(&d, &Limits::default()).unwrap();
assert_eq!(decode(&data, &Limits::default()).unwrap(), d);
}
}
#[test]
fn shared_contour_width_and_legacy_repair() {
for version in VERSIONS {
for n in [256, 32768, 65536] {
let mut d = empty(version);
d.world.computed = true;
d.world.brushes.push(Brush {
planes: vec![
Plane {
polygon: Some(vec![[1.0, 2.0, 3.0]; n]),
..Plane::default()
},
Plane {
polygon: Some(vec![[4.0, 5.0, 6.0]]),
..Plane::default()
},
],
});
let data = encode(&d, &Limits::default()).unwrap();
assert_eq!(decode(&data, &Limits::default()).unwrap(), d);
}
}
}
#[test]
fn patch_dimensions_and_tessellation_widths() {
for version in VERSIONS {
for n in [255, 256, 32768, 65536] {
let mut d = empty(version);
d.world.patches.push(Patch9 {
anchors_u: n,
anchors_v: 0,
quads_u: n,
quads_v: 2,
..Patch9::default()
});
assert_eq!(
decode(&encode(&d, &Limits::default()).unwrap(), &Limits::default()).unwrap(),
d
);
}
}
}
#[test]
fn strict_strings_on_read_and_write() {
for text in ["é", "a\0b"] {
let mut d = empty(Version::V201512200);
d.world.string_properties.push(text.into());
assert!(encode(&d, &Limits::default()).is_err());
}
for tail in [vec![1, b'a'], vec![1, 0xff, 0]] {
let mut data = b"Mb3World".to_vec();
data.extend_from_slice(&201512200i32.to_le_bytes());
data.push(0);
data.extend(tail);
assert!(decode(&data, &Limits::default()).is_err());
}
let mut d = empty(Version::V201512200);
d.world.string_properties = vec!["same".into(), "same".into()];
assert_eq!(
decode(&encode(&d, &Limits::default()).unwrap(), &Limits::default()).unwrap(),
d
);
}
fn sparse_wire(entries: &[u8]) -> Vec<u8> {
let mut data = b"Mb3World".to_vec();
data.extend_from_slice(&201512200i32.to_le_bytes());
data.extend_from_slice(b"\0\x02a\0b\0\0");
data.extend_from_slice(&1i32.to_le_bytes());
data.extend_from_slice(&[0, 1]);
data.extend_from_slice(entries);
data.push(255);
data.extend_from_slice(&[0; 36]);
data.extend_from_slice(&0i32.to_le_bytes());
data
}
#[test]
fn binary_properties_sorted_but_duplicates_and_bad_indices_rejected() {
let d = decode(&sparse_wire(b"\x01B\0\0A\0"), &Limits::default()).unwrap();
assert_eq!(
d.world.brushes[0].planes[0].strings,
vec![
Property {
index: 0,
value: "A".into()
},
Property {
index: 1,
value: "B".into()
}
]
);
for entries in [b"\0a\0\0b\0".as_slice(), b"\x02a\0"] {
assert!(decode(&sparse_wire(entries), &Limits::default()).is_err());
}
let mut d = d;
d.world.brushes[0].planes[0].strings.reverse();
assert!(matches!(
encode(&d, &Limits::default()).unwrap_err().kind,
ErrorKind::UnsortedProperties
));
d.world.brushes[0].planes[0].strings = vec![
Property {
index: 0,
value: "a".into()
};
2
];
assert!(matches!(
encode(&d, &Limits::default()).unwrap_err().kind,
ErrorKind::DuplicateProperty(0)
));
d.world.brushes[0].planes[0].strings[0].index = 9;
assert!(matches!(
encode(&d, &Limits::default()).unwrap_err().kind,
ErrorKind::PropertyIndex(9)
));
}
#[test]
fn legacy_downgrade_refuses_properties() {
let mut d = rich(Version::V201512200);
d.version = Version::V201501280;
assert!(matches!(
encode(&d, &Limits::default()).unwrap_err().kind,
ErrorKind::LegacyProperties
));
d.world.string_properties.clear();
d.world.uv_properties.clear();
assert!(matches!(
encode(&d, &Limits::default()).unwrap_err().kind,
ErrorKind::LegacyProperties
));
}
#[test]
fn refuses_inconsistent_computed_arrays() {
for version in VERSIONS {
for mode in 0..5 {
let mut d = rich(version);
match mode {
0 => d.world.computed = false,
1 => d.world.brushes[0].planes[0].polygon = None,
2 => d.world.patches[0].computed = None,
3 => {
d.world.patches[0].anchors.pop();
}
_ => {
d.world.patches[0].computed.as_mut().unwrap().normals.pop();
}
}
assert!(encode(&d, &Limits::default()).is_err());
}
}
}
#[test]
fn overflow_and_impossible_dimensions_are_errors() {
for version in VERSIONS {
let mut d = empty(version);
d.world.computed = true;
d.world.patches.push(Patch9 {
anchors_u: 7,
anchors_v: 7,
quads_u: i32::MAX as u32,
quads_v: i32::MAX as u32,
anchors: vec![[0.0; 3]; 49],
computed: Some(PatchComputed::default()),
});
assert!(encode(&d, &Limits::default()).is_err());
d.world.computed = false;
d.world.patches[0].computed = None;
d.world.patches[0].quads_u = u32::MAX;
assert!(matches!(
encode(&d, &Limits::default()).unwrap_err().kind,
ErrorKind::CountOverflow
));
}
}
#[test]
fn nonfinite_and_finite_extremes() {
for f in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let mut d = rich(Version::V201512200);
d.world.brushes[0].planes[0].p0[0] = f;
assert!(matches!(
encode(&d, &Limits::default()).unwrap_err().kind,
ErrorKind::NonFinite
));
let mut data = sparse_wire(&[]);
// The final 40 bytes are the plane vectors and the patch count.
let pos = data.len() - 40;
data[pos..pos + 4].copy_from_slice(&f.to_le_bytes());
assert!(matches!(
decode(&data, &Limits::default()).unwrap_err().kind,
ErrorKind::NonFinite
));
}
let mut d = rich(Version::V201501280);
d.world.brushes[0].planes[0].p0 = [f32::MAX, f32::MIN_POSITIVE, f32::from_bits(1)];
assert_eq!(
decode(&encode(&d, &Limits::default()).unwrap(), &Limits::default()).unwrap(),
d
);
}
#[test]
fn each_resource_limit_is_enforced() {
let d = rich(Version::V201512200);
let data = encode(&d, &Limits::default()).unwrap();
for limits in [
Limits {
max_bytes: data.len() - 1,
..Limits::default()
},
Limits {
max_items: 1,
..Limits::default()
},
Limits {
max_string_bytes: 1,
..Limits::default()
},
] {
assert!(decode(&data, &limits).is_err());
assert!(encode(&d, &limits).is_err());
}
assert!(
decode(
&data,
&Limits {
max_owned_bytes: 1,
..Limits::default()
}
)
.is_err()
);
let mut input = Cursor::new(&data);
assert!(
read(
&mut input,
&Limits {
max_bytes: 20,
..Limits::default()
}
)
.is_err()
);
assert_eq!(input.position(), 21);
}
#[test]
fn exact_string_and_item_limits() {
let mut d = empty(Version::V201512200);
d.world.string_properties.push("abcd".into());
let limits = Limits {
max_items: 1,
max_string_bytes: 4,
..Limits::default()
};
let data = encode(&d, &limits).unwrap();
assert_eq!(decode(&data, &limits).unwrap(), d);
assert!(
decode(
&data,
&Limits {
max_string_bytes: 3,
..limits
}
)
.is_err()
);
}
struct Choppy {
bytes: Cursor<Vec<u8>>,
interrupt: bool,
}
impl Read for Choppy {
fn read(&mut self, b: &mut [u8]) -> io::Result<usize> {
if self.interrupt {
self.interrupt = false;
return Err(io::ErrorKind::Interrupted.into());
}
let n = b.len().min(1);
self.bytes.read(&mut b[..n])
}
}
impl Write for Choppy {
fn write(&mut self, b: &[u8]) -> io::Result<usize> {
if self.interrupt {
self.interrupt = false;
return Err(io::ErrorKind::Interrupted.into());
}
self.bytes.write(&b[..b.len().min(1)])
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn streaming_short_reads_writes_and_interruptions() {
let d = rich(Version::V201512200);
let data = encode(&d, &Limits::default()).unwrap();
assert_eq!(
read(
Choppy {
bytes: Cursor::new(data.clone()),
interrupt: true
},
&Limits::default()
)
.unwrap(),
d
);
let mut target = Choppy {
bytes: Cursor::new(Vec::new()),
interrupt: true,
};
write(&mut target, &d, &Limits::default()).unwrap();
assert_eq!(target.bytes.into_inner(), data);
}
#[test]
fn invalid_models_do_not_touch_writer() {
let mut d = rich(Version::V201501280);
d.world.patches[0].anchors.clear();
let mut target = vec![1, 2, 3];
assert!(write(&mut target, &d, &Limits::default()).is_err());
assert_eq!(target, [1, 2, 3]);
}
struct Broken {
written: usize,
}
impl Write for Broken {
fn write(&mut self, b: &[u8]) -> io::Result<usize> {
if self.written >= 7 {
return Err(io::ErrorKind::BrokenPipe.into());
}
let n = b.len().min(7 - self.written);
self.written += n;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Read for Broken {
fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
Err(io::ErrorKind::BrokenPipe.into())
}
}
#[test]
fn io_errors_and_offsets_propagate() {
let e = write(
Broken { written: 0 },
&empty(Version::V201501280),
&Limits::default(),
)
.unwrap_err();
assert_eq!(e.offset, 7);
assert!(matches!(e.kind, ErrorKind::Io(_)));
assert!(matches!(
read(Broken { written: 0 }, &Limits::default())
.unwrap_err()
.kind,
ErrorKind::Io(_)
));
assert!(matches!(
write(
[0u8; 0].as_mut_slice(),
&empty(Version::V201501280),
&Limits::default()
)
.unwrap_err()
.kind,
ErrorKind::Io(_)
));
}
#[test]
fn deterministic_mutation_smoke_no_panics_or_unbounded_allocations() {
let limits = Limits {
max_bytes: 2048,
max_string_bytes: 64,
max_items: 256,
max_owned_bytes: 32768,
};
let seeds: Vec<_> = VERSIONS
.iter()
.map(|v| encode(&rich(*v), &limits).unwrap())
.collect();
let mut state = 0x37ab12cdu32;
for i in 0..5000 {
let mut data = seeds[i % seeds.len()].clone();
for _ in 0..1 + i % 8 {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
let at = state as usize % data.len();
data[at] ^= (state >> 24) as u8;
}
if let Ok(d) = decode(&data, &limits) {
let canonical = encode(&d, &limits).unwrap();
assert_eq!(decode(&canonical, &limits).unwrap(), d);
assert_eq!(
encode(&decode(&canonical, &limits).unwrap(), &limits).unwrap(),
canonical
);
}
}
}
#[test]
fn valid_nonminimal_widths_are_normalized_and_patch_flags_are_strict() {
let canonical = encode(&empty(Version::V201512200), &Limits::default()).unwrap();
let mut wide = canonical.clone();
wide[12] = 2; // S uses u16 rather than u8, while its count is zero.
wide.insert(13, 0);
let d = decode(&wide, &Limits::default()).unwrap();
assert_eq!(encode(&d, &Limits::default()).unwrap(), canonical);
for version in VERSIONS {
let mut d = empty(version);
d.world.patches.push(Patch9::default());
let data = encode(&d, &Limits::default()).unwrap();
let flag_at = if version == Version::V201501280 {
21
} else {
23
};
for flags in [3, 12, 0x80] {
let mut bad = data.clone();
bad[flag_at] = flags;
assert!(decode(&bad, &Limits::default()).is_err());
}
}
}
#[test]
fn reader_rejects_computed_grid_arithmetic_overflow() {
for version in VERSIONS {
let mut data = b"Mb3World".to_vec();
data.extend_from_slice(&(version as u32).to_le_bytes());
data.push(1);
if version == Version::V201512200 {
data.extend_from_slice(&[0, 0]);
}
data.extend_from_slice(&0i32.to_le_bytes());
data.extend_from_slice(&1i32.to_le_bytes());
data.extend_from_slice(&[8, 7, 7]);
data.extend_from_slice(&i32::MAX.to_le_bytes());
data.extend_from_slice(&i32::MAX.to_le_bytes());
data.extend_from_slice(&[0; 49 * 12]);
assert!(matches!(
decode(&data, &Limits::default()).unwrap_err().kind,
ErrorKind::CountOverflow
));
}
}