1use std::io::Write;
11
12use flate2::write::ZlibEncoder;
13use flate2::Compression;
14
15use crate::error::{DagError, DagResult};
16use crate::dag::{DagLevel, MeshletDag};
17use crate::types::{MESHLET_MAX_TRIANGLES_LIMIT, MESHLET_MAX_VERTICES_LIMIT};
18
19pub const MAGIC: [u8; 4] = *b"DGC1";
21pub const FORMAT_VERSION: u32 = 1;
23pub const FILE_HEADER_SIZE: usize = 64;
25pub const SECTION_HEADER_SIZE: usize = 88;
27pub const PAYLOAD_ALIGNMENT: usize = 8;
29
30pub const SECTION_KIND_LEVEL: u32 = 0;
32pub const SECTION_KIND_PARENTS: u32 = 1;
34
35pub const FLAG_ZLIB: u32 = 1 << 0;
37
38pub const NO_PARENT: u32 = u32::MAX;
40
41#[derive(Debug, Clone, Copy)]
43pub struct DgcWriteOptions {
44 pub compress: bool,
46}
47
48impl Default for DgcWriteOptions {
49 fn default() -> Self {
50 Self { compress: true }
51 }
52}
53
54pub fn write_dgc(dag: &MeshletDag, options: &DgcWriteOptions) -> DagResult<Vec<u8>> {
59 let level_count = dag.levels.len();
60 if level_count == 0 {
61 return Err(DagError::dgc_format("cannot serialize an empty DAG"));
62 }
63 let parent_pair_count = dag.parents_by_level.len();
64 if parent_pair_count != level_count.saturating_sub(1) {
65 return Err(DagError::dgc_format(format!(
66 "parents_by_level has {parent_pair_count} entries for {level_count} levels; expected {}",
67 level_count.saturating_sub(1)
68 )));
69 }
70 let section_count = level_count + parent_pair_count;
71 let level0 = &dag.levels[0];
72 let source_vertex_count = level0.positions.len() / 3;
73 let source_triangle_count = level0.indices.len() / 3;
74 if source_vertex_count > u32::MAX as usize || source_triangle_count > u32::MAX as usize {
75 return Err(DagError::overflow("source counts exceed u32"));
76 }
77
78 let mut sections: Vec<SectionRecord> = Vec::with_capacity(section_count);
80 for level in &dag.levels {
81 if level.meshlet_count > u32::MAX as usize {
82 return Err(DagError::overflow("meshlet count exceeds u32"));
83 }
84 let mut payload = Vec::new();
85 let counts = [
86 level.positions.len(),
87 level.indices.len(),
88 level.descriptors.len(),
89 level.vertex_remap.len(),
90 level.local_triangle_indices.len(),
91 level.bounds.len(),
92 level.source_triangles.len(),
93 level.cluster_source_spans.len(),
94 ];
95 extend_le_f32(&mut payload, &level.positions);
96 extend_le_u32(&mut payload, &level.indices);
97 extend_le_u32(&mut payload, &level.descriptors);
98 extend_le_u32(&mut payload, &level.vertex_remap);
99 extend_le_u32(&mut payload, &level.local_triangle_indices);
100 extend_le_f32(&mut payload, &level.bounds);
101 extend_le_u32(&mut payload, &level.source_triangles);
102 extend_le_u32(&mut payload, &level.cluster_source_spans);
103 sections.push(SectionRecord {
104 kind: SECTION_KIND_LEVEL,
105 level: level.level,
106 error: level.error,
107 counts,
108 raw: payload,
109 payload_offset: 0,
110 });
111 }
112 for (k, parents) in dag.parents_by_level.iter().enumerate() {
113 let k = k as u32;
114 if k + 1 >= level_count as u32 {
115 return Err(DagError::dgc_format("parents level index out of range"));
116 }
117 let coarse_count = dag.levels[k as usize + 1].meshlet_count;
118 for &parent in parents {
119 if parent != NO_PARENT && parent as usize >= coarse_count {
120 return Err(DagError::dgc_format(format!(
121 "parent {parent} out of range for coarse level {} ({coarse_count} clusters)",
122 k + 1
123 )));
124 }
125 }
126 let mut payload = Vec::new();
127 extend_le_u32(&mut payload, parents);
128 sections.push(SectionRecord {
129 kind: SECTION_KIND_PARENTS,
130 level: k,
131 error: 0.0,
132 counts: [parents.len(), 0, 0, 0, 0, 0, 0, 0],
133 raw: payload,
134 payload_offset: 0,
135 });
136 }
137
138 let header_span = FILE_HEADER_SIZE + SECTION_HEADER_SIZE * section_count;
140 let mut offset = header_span;
141 let mut stored_payloads: Vec<Vec<u8>> = Vec::with_capacity(sections.len());
142 for section in &mut sections {
143 let stored = if options.compress {
144 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::new(6));
145 encoder
146 .write_all(§ion.raw)
147 .map_err(|e| DagError::Io { path: std::path::PathBuf::from("<memory>"), source: e })?;
148 encoder.finish().map_err(|e| DagError::Io {
149 path: std::path::PathBuf::from("<memory>"),
150 source: std::io::Error::other(e),
151 })?
152 } else {
153 section.raw.clone()
154 };
155 offset = align_up(offset, PAYLOAD_ALIGNMENT);
156 section.payload_offset = offset as u64;
157 offset += stored.len();
158 stored_payloads.push(stored);
159 }
160 let total_size = offset;
161 if total_size as u64 > 64_u64 * 1024 * 1024 * 1024 {
163 return Err(DagError::overflow("serialized DAG exceeds 64 GiB sanity bound"));
164 }
165
166 let mut out = Vec::with_capacity(total_size);
167 out.extend_from_slice(&MAGIC);
169 extend_le_u32(&mut out, &[FORMAT_VERSION]);
170 let mut flags = 0u32;
171 if options.compress {
172 flags |= FLAG_ZLIB;
173 }
174 extend_le_u32(&mut out, &[flags]);
175 extend_le_u32(&mut out, &[level_count as u32]);
176 extend_le_u32(&mut out, &[parent_pair_count as u32]);
177 extend_le_u32(&mut out, &[source_vertex_count as u32]);
178 extend_le_u32(&mut out, &[source_triangle_count as u32]);
179 extend_le_u32(&mut out, &[level0.max_vertices]);
180 extend_le_u32(&mut out, &[level0.max_triangles]);
181 extend_le_u64(&mut out, &[total_size as u64]);
182 out.extend_from_slice(&[0u8; 20]);
184 debug_assert_eq!(out.len(), FILE_HEADER_SIZE);
185
186 for (index, (section, stored)) in sections.iter().zip(&stored_payloads).enumerate() {
188 extend_le_u32(&mut out, &[section.kind]);
189 extend_le_u32(&mut out, &[section.level]);
190 extend_le_f64(&mut out, &[section.error]);
191 extend_le_u32(&mut out, §ion.counts.map(|c| u32::try_from(c).unwrap_or(u32::MAX)));
192 extend_le_u64(&mut out, &[section.raw.len() as u64]);
193 extend_le_u64(&mut out, &[stored.len() as u64]);
194 extend_le_u64(&mut out, &[section.payload_offset]);
195 extend_le_u32(&mut out, &[crc32c(§ion.raw)]);
196 extend_le_u32(&mut out, &[0]);
198 extend_le_u32(&mut out, &[0]);
199 extend_le_u32(&mut out, &[0]);
200 debug_assert_eq!(out.len(), FILE_HEADER_SIZE + (index + 1) * SECTION_HEADER_SIZE);
201 }
202 debug_assert_eq!(out.len(), header_span);
203
204 for (section, stored) in sections.iter().zip(&stored_payloads) {
206 let aligned = align_up(out.len(), PAYLOAD_ALIGNMENT);
207 out.resize(aligned, 0);
208 debug_assert_eq!(out.len() as u64, section.payload_offset);
209 out.extend_from_slice(stored);
210 }
211 debug_assert_eq!(out.len(), total_size);
212 Ok(out)
213}
214
215pub fn read_dgc(bytes: &[u8]) -> DagResult<MeshletDag> {
220 let mut cursor = Reader::new(bytes);
221 let mut magic = [0u8; 4];
222 cursor.read_exact_into(&mut magic)?;
223 if magic != MAGIC {
224 return Err(DagError::dgc_format(format!("bad magic {magic:?}, expected {MAGIC:?}")));
225 }
226 let version = cursor.read_u32()?;
227 if version != FORMAT_VERSION {
228 return Err(DagError::dgc_format(format!("unsupported version {version}, expected {FORMAT_VERSION}")));
229 }
230 let flags = cursor.read_u32()?;
231 if flags & !FLAG_ZLIB != 0 {
232 return Err(DagError::dgc_format(format!("unknown flags {flags:#x}")));
233 }
234 let compressed = flags & FLAG_ZLIB != 0;
235 let level_count = cursor.read_u32()? as usize;
236 let parent_pair_count = cursor.read_u32()? as usize;
237 if level_count == 0 {
238 return Err(DagError::dgc_format("zero level count"));
239 }
240 if parent_pair_count != level_count - 1 {
241 return Err(DagError::dgc_format(format!(
242 "parent pairs {parent_pair_count} inconsistent with {level_count} levels"
243 )));
244 }
245 let source_vertex_count = cursor.read_u32()? as usize;
246 let source_triangle_count = cursor.read_u32()? as usize;
247 let max_vertices = cursor.read_u32()?;
248 let max_triangles = cursor.read_u32()?;
249 let total_size = cursor.read_u64()? as usize;
250 let reserved = cursor.read_bytes(20)?;
251 if reserved.iter().any(|&b| b != 0) {
252 return Err(DagError::dgc_format("header reserved bytes must be zero"));
253 }
254 if total_size != bytes.len() {
255 return Err(DagError::dgc_format(format!(
256 "size lock mismatch: header says {total_size}, file is {}",
257 bytes.len()
258 )));
259 }
260 if max_vertices > MESHLET_MAX_VERTICES_LIMIT || max_triangles > MESHLET_MAX_TRIANGLES_LIMIT {
261 return Err(DagError::dgc_format(
262 "cluster limits exceed schema caps (file from a newer build?)",
263 ));
264 }
265
266 let section_count = level_count + parent_pair_count;
267 let header_span = FILE_HEADER_SIZE + SECTION_HEADER_SIZE * section_count;
268 if bytes.len() < header_span {
269 return Err(DagError::dgc_format(format!(
270 "truncated: {} bytes < header span {header_span}",
271 bytes.len()
272 )));
273 }
274
275 let mut levels: Vec<DagLevel> = Vec::with_capacity(level_count);
276 let mut parents_by_level: Vec<Vec<u32>> = Vec::with_capacity(parent_pair_count);
277 for index in 0..section_count {
278 let header_base = FILE_HEADER_SIZE + SECTION_HEADER_SIZE * index;
279 let mut header = Reader::new(&bytes[header_base..header_base + SECTION_HEADER_SIZE]);
280 let kind = header.read_u32()?;
281 let level_index = header.read_u32()?;
282 let error = header.read_f64()?;
283 let mut counts = [0u32; 8];
284 for count in &mut counts {
285 *count = header.read_u32()?;
286 }
287 let raw_size = header.read_u64()? as usize;
288 let stored_size = header.read_u64()? as usize;
289 let payload_offset = header.read_u64()? as usize;
290 let crc = header.read_u32()?;
291 let reserved_a = header.read_u32()?;
292 let reserved_b = header.read_u32()?;
293 let reserved_c = header.read_u32()?;
294 if reserved_a != 0 || reserved_b != 0 || reserved_c != 0 {
295 return Err(DagError::dgc_format("section reserved bytes must be zero"));
296 }
297 if !payload_offset.is_multiple_of(PAYLOAD_ALIGNMENT) {
298 return Err(DagError::dgc_format("payload offset must be 8-byte aligned"));
299 }
300 if payload_offset.checked_add(stored_size).is_none_or(|end| end > bytes.len()) {
301 return Err(DagError::dgc_format(format!(
302 "section {index} payload [{payload_offset}, {} ) exceeds file size {}",
303 payload_offset + stored_size,
304 bytes.len()
305 )));
306 }
307 let stored = &bytes[payload_offset..payload_offset + stored_size];
308 let raw: Vec<u8> = if compressed {
309 decode_zlib(stored, raw_size)?
310 } else {
311 if stored_size != raw_size {
312 return Err(DagError::dgc_format(format!(
313 "section {index}: uncompressed stored size {stored_size} != raw size {raw_size}"
314 )));
315 }
316 stored.to_vec()
317 };
318 if raw.len() != raw_size {
319 return Err(DagError::dgc_format(format!(
320 "section {index}: raw size {} != header {raw_size}",
321 raw.len()
322 )));
323 }
324 let actual_crc = crc32c(&raw);
325 if actual_crc != crc {
326 return Err(DagError::dgc_format(format!(
327 "section {index} crc mismatch: file {crc:#010x}, computed {actual_crc:#010x}"
328 )));
329 }
330
331 match kind {
332 SECTION_KIND_LEVEL => {
333 let mut payload = Reader::new(&raw);
334 let [position_count, index_count, descriptor_count, remap_count, local_tri_count, bounds_count, source_tri_count, span_count] =
335 counts;
336 let expected_raw = 4 * (position_count + index_count + descriptor_count + remap_count
337 + local_tri_count + bounds_count + source_tri_count + span_count)
338 as u64;
339 if expected_raw != raw_size as u64 {
340 return Err(DagError::dgc_format(format!(
341 "level section {level_index}: counts imply {expected_raw} bytes, header says {raw_size}"
342 )));
343 }
344 let positions = payload.read_f32_vec(position_count as usize)?;
345 let indices = payload.read_u32_vec(index_count as usize)?;
346 let descriptors = payload.read_u32_vec(descriptor_count as usize)?;
347 let vertex_remap = payload.read_u32_vec(remap_count as usize)?;
348 let local_triangle_indices = payload.read_u32_vec(local_tri_count as usize)?;
349 let bounds = payload.read_f32_vec(bounds_count as usize)?;
350 let source_triangles = payload.read_u32_vec(source_tri_count as usize)?;
351 let cluster_source_spans = payload.read_u32_vec(span_count as usize)?;
352 if payload.remaining() != 0 {
353 return Err(DagError::dgc_format(format!(
354 "level section {level_index}: {} trailing bytes",
355 payload.remaining()
356 )));
357 }
358 if descriptor_count / 4 != span_count / 2 {
360 return Err(DagError::dgc_format(format!(
361 "level section {level_index}: {} span pairs inconsistent with {} meshlets",
362 span_count / 2,
363 descriptor_count / 4
364 )));
365 }
366 if position_count % 3 != 0 || index_count % 3 != 0 {
367 return Err(DagError::dgc_format(format!(
368 "level section {level_index}: position/index counts must be triples"
369 )));
370 }
371 let mut expected_start = 0u32;
372 for span in cluster_source_spans.chunks_exact(2) {
373 if span[0] != expected_start || span[1] < span[0] {
374 return Err(DagError::dgc_format(format!(
375 "level section {level_index}: cluster source spans not a prefix-sum chain"
376 )));
377 }
378 expected_start = span[1];
379 }
380 if expected_start != index_count / 3 {
381 return Err(DagError::dgc_format(format!(
382 "level section {level_index}: spans cover {} triangles, indices have {}",
383 expected_start,
384 index_count / 3
385 )));
386 }
387 for &index in &indices {
388 if index >= position_count / 3 {
389 return Err(DagError::dgc_format(format!(
390 "level section {level_index}: index {index} out of vertex range"
391 )));
392 }
393 }
394 levels.push(DagLevel {
395 level: level_index,
396 error,
397 positions,
398 indices,
399 meshlet_count: descriptor_count as usize / 4,
400 max_vertices,
401 max_triangles,
402 descriptors,
403 vertex_remap,
404 local_triangle_indices,
405 bounds,
406 source_triangles,
407 cluster_source_spans,
408 });
409 }
410 SECTION_KIND_PARENTS => {
411 if levels.len() != level_count {
413 return Err(DagError::dgc_format(
414 "parents section encountered before all level sections (order contract violated)",
415 ));
416 }
417 let fine = level_index as usize;
418 if fine >= level_count - 1 {
419 return Err(DagError::dgc_format(format!(
420 "parents section references level {fine} as fine side, but last level is {}",
421 level_count - 1
422 )));
423 }
424 let mut payload = Reader::new(&raw);
425 let parents = payload.read_u32_vec(counts[0] as usize)?;
426 if payload.remaining() != 0 {
427 return Err(DagError::dgc_format("parents section has trailing bytes"));
428 }
429 let fine_clusters = levels[fine].meshlet_count;
430 if parents.len() != fine_clusters {
431 return Err(DagError::dgc_format(format!(
432 "parents section for level {fine}: {} entries, fine level has {fine_clusters} clusters",
433 parents.len()
434 )));
435 }
436 let coarse_count = levels[fine + 1].meshlet_count;
437 for (cluster, &parent) in parents.iter().enumerate() {
438 if parent != NO_PARENT && parent as usize >= coarse_count {
439 return Err(DagError::dgc_format(format!(
440 "parents section for level {fine}: cluster {cluster} parent {parent} out of range"
441 )));
442 }
443 }
444 parents_by_level.push(parents);
445 }
446 other => {
447 return Err(DagError::dgc_format(format!("unknown section kind {other}")));
448 }
449 }
450 }
451 if levels.len() != level_count || parents_by_level.len() != parent_pair_count {
452 return Err(DagError::dgc_format("section count mismatch after parse"));
453 }
454 if levels[0].positions.len() / 3 != source_vertex_count
456 || levels[0].indices.len() / 3 != source_triangle_count
457 {
458 return Err(DagError::dgc_format(
459 "level 0 geometry size disagrees with file header",
460 ));
461 }
462 Ok(MeshletDag { levels, parents_by_level })
463}
464
465struct SectionRecord {
468 kind: u32,
469 level: u32,
470 error: f64,
471 counts: [usize; 8],
472 raw: Vec<u8>,
473 payload_offset: u64,
474}
475
476fn align_up(value: usize, alignment: usize) -> usize {
477 value.div_ceil(alignment) * alignment
478}
479
480fn extend_le_u32(out: &mut Vec<u8>, values: &[u32]) {
481 for &value in values {
482 out.extend_from_slice(&value.to_le_bytes());
483 }
484}
485
486fn extend_le_u64(out: &mut Vec<u8>, values: &[u64]) {
487 for &value in values {
488 out.extend_from_slice(&value.to_le_bytes());
489 }
490}
491
492fn extend_le_f32(out: &mut Vec<u8>, values: &[f32]) {
493 for &value in values {
494 out.extend_from_slice(&value.to_le_bytes());
495 }
496}
497
498fn extend_le_f64(out: &mut Vec<u8>, values: &[f64]) {
499 for &value in values {
500 out.extend_from_slice(&value.to_le_bytes());
501 }
502}
503
504struct Reader<'a> {
506 bytes: &'a [u8],
507 pos: usize,
508}
509
510impl<'a> Reader<'a> {
511 fn new(bytes: &'a [u8]) -> Self {
512 Self { bytes, pos: 0 }
513 }
514
515 fn read_bytes(&mut self, len: usize) -> DagResult<&'a [u8]> {
516 let end = self.pos.checked_add(len).ok_or_else(|| DagError::dgc_format("size overflow"))?;
517 if end > self.bytes.len() {
518 return Err(DagError::dgc_format(format!(
519 "truncated: need {len} bytes at offset {}, only {} remain",
520 self.pos,
521 self.bytes.len() - self.pos
522 )));
523 }
524 let slice = &self.bytes[self.pos..end];
525 self.pos = end;
526 Ok(slice)
527 }
528
529 fn read_exact_into(&mut self, out: &mut [u8]) -> DagResult<()> {
530 let slice = self.read_bytes(out.len())?;
531 out.copy_from_slice(slice);
532 Ok(())
533 }
534
535 fn read_u32(&mut self) -> DagResult<u32> {
536 let slice = self.read_bytes(4)?;
537 Ok(u32::from_le_bytes(slice.try_into().expect("4 bytes")))
538 }
539
540 fn read_u64(&mut self) -> DagResult<u64> {
541 let slice = self.read_bytes(8)?;
542 Ok(u64::from_le_bytes(slice.try_into().expect("8 bytes")))
543 }
544
545 fn read_f64(&mut self) -> DagResult<f64> {
546 Ok(f64::from_bits(self.read_u64()?))
547 }
548
549 fn read_u32_vec(&mut self, count: usize) -> DagResult<Vec<u32>> {
550 let slice = self.read_bytes(count * 4)?;
551 Ok(slice
552 .chunks_exact(4)
553 .map(|c| u32::from_le_bytes(c.try_into().expect("4 bytes")))
554 .collect())
555 }
556
557 fn read_f32_vec(&mut self, count: usize) -> DagResult<Vec<f32>> {
558 let slice = self.read_bytes(count * 4)?;
559 Ok(slice
560 .chunks_exact(4)
561 .map(|c| f32::from_le_bytes(c.try_into().expect("4 bytes")))
562 .collect())
563 }
564
565 fn remaining(&self) -> usize {
566 self.bytes.len() - self.pos
567 }
568}
569
570fn decode_zlib(data: &[u8], raw_size: usize) -> DagResult<Vec<u8>> {
572 use std::io::Read;
573 let decoder = flate2::read::ZlibDecoder::new(data);
574 let mut out = Vec::with_capacity(raw_size.min(1 << 28));
575 decoder
576 .take(raw_size as u64 + 1)
577 .read_to_end(&mut out)
578 .map_err(|e| DagError::dgc_format(format!("zlib decode failed: {e}")))?;
579 if out.len() != raw_size {
580 return Err(DagError::dgc_format(format!(
581 "zlib payload size {} != header raw size {raw_size}",
582 out.len()
583 )));
584 }
585 Ok(out)
586}
587
588#[must_use]
590pub fn crc32c(data: &[u8]) -> u32 {
591 const POLY: u32 = 0x82f6_3b78; static TABLE: std::sync::OnceLock<[u32; 256]> = std::sync::OnceLock::new();
593 let table = TABLE.get_or_init(|| {
594 let mut table = [0u32; 256];
595 for (i, entry) in table.iter_mut().enumerate() {
596 let mut crc = i as u32;
597 for _ in 0..8 {
598 crc = if crc & 1 != 0 { (crc >> 1) ^ POLY } else { crc >> 1 };
599 }
600 *entry = crc;
601 }
602 table
603 });
604 let mut crc = 0xFFFF_FFFFu32;
605 for &byte in data {
606 crc = (crc >> 8) ^ table[((crc ^ u32::from(byte)) & 0xFF) as usize];
607 }
608 crc ^ 0xFFFF_FFFF
609}
610
611impl DagLevel {
612 #[must_use]
614 pub fn max_vertices(&self) -> u32 {
615 self.max_vertices
616 }
617
618 #[must_use]
620 pub fn max_triangles(&self) -> u32 {
621 self.max_triangles
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use crate::dag::{build_meshlet_dag, DagOptions};
629 use crate::types::IndexedGeometry;
630
631 fn sphere_geometry(segments: usize, rings: usize) -> IndexedGeometry {
632 let (positions, indices) = crate::simplify::test_support::test_sphere(segments, rings);
633 IndexedGeometry { positions, indices }
634 }
635
636 fn sample_dag() -> MeshletDag {
637 build_meshlet_dag(&sphere_geometry(24, 12), &DagOptions { levels: Some(4), ..Default::default() })
638 .expect("dag")
639 }
640
641 #[test]
642 fn roundtrip_all_levels_bit_exact() {
643 let dag = sample_dag();
644 for compress in [true, false] {
645 let bytes = write_dgc(&dag, &DgcWriteOptions { compress }).expect("write");
646 let back = read_dgc(&bytes).expect("read");
647 assert_eq!(back.levels.len(), dag.levels.len());
648 for (a, b) in dag.levels.iter().zip(&back.levels) {
649 assert_eq!(a.level, b.level);
650 assert_eq!(a.error.to_bits(), b.error.to_bits());
651 assert_eq!(a.positions, b.positions);
652 assert_eq!(a.indices, b.indices);
653 assert_eq!(a.descriptors, b.descriptors);
654 assert_eq!(a.vertex_remap, b.vertex_remap);
655 assert_eq!(a.local_triangle_indices, b.local_triangle_indices);
656 assert_eq!(a.bounds, b.bounds);
657 assert_eq!(a.source_triangles, b.source_triangles);
658 assert_eq!(a.cluster_source_spans, b.cluster_source_spans);
659 }
660 assert_eq!(back.parents_by_level, dag.parents_by_level);
661 }
662 }
663
664 #[test]
665 fn truncated_file_is_rejected() {
666 let bytes = write_dgc(&sample_dag(), &DgcWriteOptions::default()).expect("write");
667 assert!(read_dgc(&bytes[..bytes.len() - 1]).is_err());
668 assert!(read_dgc(&bytes[..40]).is_err());
669 }
670
671 #[test]
672 fn corrupted_byte_detected_by_crc() {
673 let mut bytes = write_dgc(&sample_dag(), &DgcWriteOptions::default()).expect("write");
674 let last = bytes.len() - 1;
675 bytes[last] ^= 0xFF;
676 assert!(read_dgc(&bytes).is_err());
677 }
678
679 #[test]
680 fn bad_magic_rejected() {
681 let mut bytes = write_dgc(&sample_dag(), &DgcWriteOptions::default()).expect("write");
682 bytes[0] = b'X';
683 let err = read_dgc(&bytes).unwrap_err();
684 assert!(err.to_string().contains("bad magic"));
685 }
686
687 #[test]
688 fn nonempty_reserved_must_be_zero() {
689 let mut bytes = write_dgc(&sample_dag(), &DgcWriteOptions::default()).expect("write");
690 bytes[0x30] = 1; let err = read_dgc(&bytes).unwrap_err();
692 assert!(err.to_string().contains("reserved"));
693 }
694
695 #[test]
696 fn empty_dag_rejected() {
697 let empty = MeshletDag { levels: vec![], parents_by_level: vec![] };
698 assert!(write_dgc(&empty, &DgcWriteOptions::default()).is_err());
699 }
700
701 #[test]
702 fn compression_shrinks_or_ties() {
703 let dag = sample_dag();
704 let raw = write_dgc(&dag, &DgcWriteOptions { compress: false }).expect("raw");
705 let zipped = write_dgc(&dag, &DgcWriteOptions { compress: true }).expect("zip");
706 assert!(zipped.len() <= raw.len(), "zlib should not inflate this payload");
707 }
708
709 #[test]
710 fn crc32c_known_vector() {
711 assert_eq!(crc32c(b"123456789"), 0xE306_9283);
713 }
714}