1use crate::topology::BrepSolid;
57use crate::{decode_solid, encode_solid, SolidNames};
58use std::collections::BTreeMap;
59
60pub const SNAPSHOT_FORMAT_VERSION: u32 = 1;
66
67const MAGIC: &[u8; 8] = b"BREPSNAP";
68
69#[derive(Debug, Clone)]
71pub struct RestoredSolid {
72 pub name: String,
73 pub solid: BrepSolid,
74}
75
76#[derive(Debug, Clone)]
80pub struct RestoredSnapshot {
81 pub solids: Vec<RestoredSolid>,
82 pub metadata: Vec<(String, serde_json::Map<String, serde_json::Value>)>,
83}
84
85pub fn snapshot_solids(solids: &[(&str, &BrepSolid)]) -> Result<String, String> {
95 let mut out = Vec::with_capacity(1024);
96 out.extend_from_slice(MAGIC);
97 push_u32(&mut out, SNAPSHOT_FORMAT_VERSION);
98 push_count(&mut out, solids.len())?;
99 let mut metadata = BTreeMap::<String, String>::new();
100 let mut capture = |name: &str| -> Result<(), String> {
101 if let Some(record) = crate::feature_pipeline::scene_metadata::own_record(name) {
102 let json = serde_json::to_string(&record)
103 .map_err(|error| format!("snapshot: metadata record for '{name}': {error}"))?;
104 metadata.insert(name.to_string(), json);
105 }
106 Ok(())
107 };
108 for (name, solid) in solids {
109 let (data, names) = encode_solid(solid)?;
110 let names_json = serde_json::to_string(&names)
111 .map_err(|error| format!("snapshot: names for '{name}': {error}"))?;
112 push_str(&mut out, name)?;
113 push_str(&mut out, &names_json)?;
114 push_count(&mut out, data.len())?;
115 for value in &data {
116 out.extend_from_slice(&value.to_le_bytes());
117 }
118 capture(name)?;
119 for face_name in names.faces.values() {
120 capture(face_name)?;
121 }
122 for edge_name in names.edges.values() {
123 capture(edge_name)?;
124 }
125 }
126 push_count(&mut out, metadata.len())?;
127 for (name, record_json) in &metadata {
128 push_str(&mut out, name)?;
129 push_str(&mut out, record_json)?;
130 }
131 Ok(seal(out))
132}
133
134fn seal(mut container: Vec<u8>) -> String {
136 let digest = fnv1a(&container);
137 container.extend_from_slice(&digest.to_le_bytes());
138 base64_encode(&container)
139}
140
141fn fnv1a(bytes: &[u8]) -> u64 {
145 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
146 for &byte in bytes {
147 hash ^= byte as u64;
148 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
149 }
150 hash
151}
152
153pub fn snapshot_resident_solids(named_handles: &[(String, u32)]) -> Result<String, String> {
156 let mut owned = Vec::with_capacity(named_handles.len());
157 for (name, handle) in named_handles {
158 let solid = crate::with_registered_solid_str(*handle, |solid| Ok(solid.clone()))?;
159 owned.push((name.as_str(), solid));
160 }
161 let borrowed: Vec<(&str, &BrepSolid)> = owned
162 .iter()
163 .map(|(name, solid)| (*name, solid))
164 .collect();
165 snapshot_solids(&borrowed)
166}
167
168pub fn restore_solids(payload: &str) -> Result<RestoredSnapshot, String> {
175 let bytes = base64_decode(payload)?;
176 if bytes.len() < MAGIC.len() + 8 {
178 return Err("snapshot: payload too short".into());
179 }
180 let (body, trailer) = bytes.split_at(bytes.len() - 8);
181 let stored = u64::from_le_bytes(trailer.try_into().expect("8-byte trailer"));
182 if fnv1a(body) != stored {
183 return Err("snapshot: payload checksum mismatch (corrupted)".into());
184 }
185 let mut reader = ByteReader {
186 data: body,
187 cursor: 0,
188 };
189 if reader.take(MAGIC.len())? != MAGIC {
190 return Err("snapshot: not a BREP snapshot payload (bad magic)".into());
191 }
192 let version = reader.u32()?;
193 if version != SNAPSHOT_FORMAT_VERSION {
194 return Err(format!(
195 "snapshot: unsupported format version {version} (expected {SNAPSHOT_FORMAT_VERSION})"
196 ));
197 }
198 let solid_count = reader.u32()? as usize;
199 let mut solids = Vec::with_capacity(solid_count.min(1024));
200 for _ in 0..solid_count {
201 let name = reader.string()?;
202 let names_json = reader.string()?;
203 let names: SolidNames = serde_json::from_str(&names_json)
204 .map_err(|error| format!("snapshot: names for '{name}': {error}"))?;
205 let data = reader.f64_vec()?;
206 let solid = decode_solid(&data, &names)
207 .map_err(|error| format!("snapshot: solid '{name}': {error}"))?;
208 solids.push(RestoredSolid { name, solid });
209 }
210 let record_count = reader.u32()? as usize;
211 let mut metadata = Vec::with_capacity(record_count.min(4096));
212 for _ in 0..record_count {
213 let name = reader.string()?;
214 let record_json = reader.string()?;
215 let record: serde_json::Map<String, serde_json::Value> =
216 serde_json::from_str(&record_json)
217 .map_err(|error| format!("snapshot: metadata record for '{name}': {error}"))?;
218 metadata.push((name, record));
219 }
220 if reader.cursor != body.len() {
221 return Err("snapshot: trailing bytes after payload".into());
222 }
223 Ok(RestoredSnapshot { solids, metadata })
224}
225
226fn push_u32(out: &mut Vec<u8>, value: u32) {
231 out.extend_from_slice(&value.to_le_bytes());
232}
233
234fn push_count(out: &mut Vec<u8>, count: usize) -> Result<(), String> {
235 let value: u32 = count
236 .try_into()
237 .map_err(|_| format!("snapshot: count {count} exceeds u32"))?;
238 push_u32(out, value);
239 Ok(())
240}
241
242fn push_str(out: &mut Vec<u8>, text: &str) -> Result<(), String> {
243 push_count(out, text.len())?;
244 out.extend_from_slice(text.as_bytes());
245 Ok(())
246}
247
248struct ByteReader<'a> {
249 data: &'a [u8],
250 cursor: usize,
251}
252
253impl<'a> ByteReader<'a> {
254 fn take(&mut self, count: usize) -> Result<&'a [u8], String> {
255 let end = self
256 .cursor
257 .checked_add(count)
258 .ok_or("snapshot: truncated payload")?;
259 let slice = self
260 .data
261 .get(self.cursor..end)
262 .ok_or("snapshot: truncated payload")?;
263 self.cursor = end;
264 Ok(slice)
265 }
266
267 fn u32(&mut self) -> Result<u32, String> {
268 let raw = self.take(4)?;
269 Ok(u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
270 }
271
272 fn string(&mut self) -> Result<String, String> {
273 let length = self.u32()? as usize;
274 let raw = self.take(length)?;
275 String::from_utf8(raw.to_vec()).map_err(|_| "snapshot: invalid UTF-8 string".into())
276 }
277
278 fn f64_vec(&mut self) -> Result<Vec<f64>, String> {
279 let count = self.u32()? as usize;
280 let raw = self.take(
281 count
282 .checked_mul(8)
283 .ok_or("snapshot: truncated payload")?,
284 )?;
285 Ok(raw
286 .chunks_exact(8)
287 .map(|chunk| {
288 f64::from_le_bytes([
289 chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
290 ])
291 })
292 .collect())
293 }
294}
295
296const B64_ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
303
304fn base64_encode(bytes: &[u8]) -> String {
305 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
306 for chunk in bytes.chunks(3) {
307 let b0 = chunk[0] as u32;
308 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
309 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
310 let triple = (b0 << 16) | (b1 << 8) | b2;
311 out.push(B64_ALPHABET[(triple >> 18) as usize & 63] as char);
312 out.push(B64_ALPHABET[(triple >> 12) as usize & 63] as char);
313 out.push(if chunk.len() > 1 {
314 B64_ALPHABET[(triple >> 6) as usize & 63] as char
315 } else {
316 '='
317 });
318 out.push(if chunk.len() > 2 {
319 B64_ALPHABET[triple as usize & 63] as char
320 } else {
321 '='
322 });
323 }
324 out
325}
326
327fn base64_value(byte: u8) -> Result<u32, String> {
328 match byte {
329 b'A'..=b'Z' => Ok((byte - b'A') as u32),
330 b'a'..=b'z' => Ok((byte - b'a') as u32 + 26),
331 b'0'..=b'9' => Ok((byte - b'0') as u32 + 52),
332 b'+' => Ok(62),
333 b'/' => Ok(63),
334 _ => Err(format!(
335 "snapshot: invalid base64 character 0x{byte:02x}"
336 )),
337 }
338}
339
340fn base64_decode(text: &str) -> Result<Vec<u8>, String> {
341 let bytes = text.as_bytes();
342 if bytes.len() % 4 != 0 {
343 return Err("snapshot: base64 length must be a multiple of 4".into());
344 }
345 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
346 for (index, quad) in bytes.chunks_exact(4).enumerate() {
347 let is_last = (index + 1) * 4 == bytes.len();
348 let padding = match (quad[2], quad[3]) {
350 (b'=', b'=') if is_last => 2,
351 (_, b'=') if is_last && quad[2] != b'=' => 1,
352 (b'=', _) => return Err("snapshot: misplaced base64 padding".into()),
353 _ => 0,
354 };
355 if quad[0] == b'=' || quad[1] == b'=' {
356 return Err("snapshot: misplaced base64 padding".into());
357 }
358 let mut triple = base64_value(quad[0])? << 18 | base64_value(quad[1])? << 12;
359 if padding < 2 {
360 triple |= base64_value(quad[2])? << 6;
361 }
362 if padding < 1 {
363 triple |= base64_value(quad[3])?;
364 }
365 out.push((triple >> 16) as u8);
366 if padding < 2 {
367 out.push((triple >> 8) as u8);
368 }
369 if padding < 1 {
370 out.push(triple as u8);
371 }
372 }
373 Ok(out)
374}
375
376