//! Explicit transactional table reordering/removal over the existing world model. use mb3_dat::{Limits, Property, Uv, World}; use std::fmt; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Error { Index, DuplicateOrder, InvalidSlots, Limit, Allocation, Overflow, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "property edit: {self:?}") } } impl std::error::Error for Error {} type Result = std::result::Result; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Report { pub dropped_string_names: usize, pub dropped_uv_names: usize, pub dropped_string_values: usize, pub dropped_uv_values: usize, } struct Budget { items: usize, bytes: usize, limits: Limits, } impl Budget { fn items(&mut self, n: usize) -> Result<()> { self.items = self.items.checked_add(n).ok_or(Error::Overflow)?; if self.items > self.limits.max_items { Err(Error::Limit) } else { Ok(()) } } fn vec(&mut self, n: usize) -> Result> { self.bytes( n.checked_mul(std::mem::size_of::()) .ok_or(Error::Overflow)?, )?; let mut v = Vec::new(); v.try_reserve_exact(n).map_err(|_| Error::Allocation)?; Ok(v) } fn bytes(&mut self, n: usize) -> Result<()> { self.bytes = self.bytes.checked_add(n).ok_or(Error::Overflow)?; if self.bytes > self.limits.max_owned_bytes { Err(Error::Limit) } else { Ok(()) } } fn string(&mut self, s: &str) -> Result { if s.len() > self.limits.max_string_bytes { return Err(Error::Limit); } self.bytes(s.len())?; let mut out = String::new(); out.try_reserve_exact(s.len()) .map_err(|_| Error::Allocation)?; out.push_str(s); Ok(out) } fn table(&mut self, names: &[String], order: &[u32]) -> Result<(Vec, Vec)> { self.items(names.len())?; self.items(order.len())?; if order.len() > i32::MAX as usize { return Err(Error::Overflow); } let mut map = self.vec(names.len())?; map.resize(names.len(), u32::MAX); let mut out = self.vec(order.len())?; for (new, old) in order.iter().enumerate() { let slot = map.get_mut(*old as usize).ok_or(Error::Index)?; if *slot != u32::MAX { return Err(Error::DuplicateOrder); } *slot = new as u32; out.push(self.string(&names[*old as usize])?); } Ok((out, map)) } } fn validate(slots: &[Property], map: &[u32]) -> Result { let mut previous = None; let mut kept = 0; for slot in slots { if previous.is_some_and(|n| n >= slot.index) { return Err(Error::InvalidSlots); } previous = Some(slot.index); let id = map.get(slot.index as usize).ok_or(Error::InvalidSlots)?; if *id != u32::MAX { kept += 1; } } Ok(kept) } struct Update { strings: Vec>, uv: Vec>, } /// New tables select old indices in the desired order. Omitted indices explicitly /// remove names and associated values. All validation/allocation precedes mutation. pub fn remap( world: &mut World, string_order: &[u32], uv_order: &[u32], limits: Limits, ) -> Result { let mut b = Budget { items: 0, bytes: 0, limits, }; let (strings, sm) = b.table(&world.string_properties, string_order)?; let (uv, um) = b.table(&world.uv_properties, uv_order)?; let mut report = Report { dropped_string_names: world.string_properties.len() - strings.len(), dropped_uv_names: world.uv_properties.len() - uv.len(), ..Report::default() }; let mut count = 0usize; for brush in &world.brushes { b.items(1)?; count = count .checked_add(brush.planes.len()) .ok_or(Error::Overflow)?; } b.items(count)?; let mut updates = b.vec(count)?; for p in world.brushes.iter().flat_map(|b| &b.planes) { b.items(p.strings.len())?; b.items(p.uv.len())?; let ns = validate(&p.strings, &sm)?; let nu = validate(&p.uv, &um)?; report.dropped_string_values = report .dropped_string_values .checked_add(p.strings.len() - ns) .ok_or(Error::Overflow)?; report.dropped_uv_values = report .dropped_uv_values .checked_add(p.uv.len() - nu) .ok_or(Error::Overflow)?; let mut ss = b.vec(ns)?; for s in &p.strings { let index = sm[s.index as usize]; if index != u32::MAX { ss.push(Property { index, value: b.string(&s.value)?, }); } } ss.sort_unstable_by_key(|p| p.index); let mut us = b.vec(nu)?; for s in &p.uv { let index = um[s.index as usize]; if index != u32::MAX { us.push(Property { index, value: s.value.clone(), }); } } us.sort_unstable_by_key(|p| p.index); updates.push(Update { strings: ss, uv: us, }); } world.string_properties = strings; world.uv_properties = uv; for (p, u) in world .brushes .iter_mut() .flat_map(|b| &mut b.planes) .zip(updates) { p.strings = u.strings; p.uv = u.uv; } Ok(report) } #[cfg(test)] mod tests { use super::*; use mb3_dat::{Brush, Document, Plane, Version}; fn world() -> World { World { string_properties: vec!["unused".into(), "a".into(), "b".into()], uv_properties: vec!["uv".into()], brushes: vec![Brush { planes: vec![Plane { p0: [1., 2., 3.], strings: vec![ Property { index: 1, value: String::new(), }, Property { index: 2, value: "value".into(), }, ], uv: vec![Property { index: 0, value: Uv { u: [1., 0., 0.], v: [0., 1., 0.], u_scale: 1., u_shift: 0., v_scale: 1., v_shift: 0., }, }], ..Default::default() }], }], ..Default::default() } } #[test] fn reorder_remove_empty_values_and_geometry_preserved() { let mut w = world(); let p = w.brushes[0].planes[0].p0; let r = remap(&mut w, &[2, 1], &[], Limits::default()).unwrap(); assert_eq!(w.string_properties, ["b", "a"]); assert_eq!( w.brushes[0].planes[0].strings, [ Property { index: 0, value: "value".into() }, Property { index: 1, value: String::new() } ] ); assert_eq!(w.brushes[0].planes[0].p0, p); assert_eq!( r, Report { dropped_string_names: 1, dropped_uv_names: 1, dropped_uv_values: 1, dropped_string_values: 0 } ); let d = Document { world: w, version: Version::V201512200, }; assert_eq!( mb3_dat::decode( &mb3_dat::encode(&d, &Limits::default()).unwrap(), &Limits::default() ) .unwrap(), d ); } #[test] fn explicit_drop_counts_and_roundtrip_permutation() { let mut w = world(); let original = w.clone(); remap(&mut w, &[2, 0, 1], &[0], Limits::default()).unwrap(); remap(&mut w, &[1, 2, 0], &[0], Limits::default()).unwrap(); assert_eq!(w, original); let r = remap(&mut w, &[], &[], Limits::default()).unwrap(); assert_eq!(r.dropped_string_values, 2); assert!(w.brushes[0].planes[0].strings.is_empty()); } #[test] fn errors_are_transactional_including_late_slots() { for order in [vec![1, 1], vec![9]] { let mut w = world(); let before = w.clone(); assert!(remap(&mut w, &order, &[0], Limits::default()).is_err()); assert_eq!(w, before); } let mut w = world(); w.brushes[0].planes[0].strings[1].index = 99; let before = w.clone(); assert!(remap(&mut w, &[0, 1, 2], &[0], Limits::default()).is_err()); assert_eq!(w, before); for l in [ Limits { max_items: 1, ..Limits::default() }, Limits { max_owned_bytes: 1, ..Limits::default() }, Limits { max_string_bytes: 1, ..Limits::default() }, ] { let mut w = world(); let before = w.clone(); assert!(remap(&mut w, &[0, 1, 2], &[0], l).is_err()); assert_eq!(w, before); } } }