1use crate::directory::TileEntry;
46use crate::compression;
47use crate::directory::{decode_directory, encode_directory};
48use crate::error::{Error, Result};
49use crate::projection::tile_geo_bounds;
50
51pub const PAGED_ROOT_VERSION: u8 = 1;
54
55pub const DESCRIPTOR_GEO_BBOX: u8 = 0;
57
58pub const DEFAULT_PAGE_ENTRIES: usize = 4096;
61
62const ROOT_HEADER_LEN: usize = 12;
65const DESCRIPTOR_LEN: usize = 52;
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct PageDescriptor {
71 pub rel_offset: u64,
75 pub length: u32,
77 pub entry_count: u32,
79 pub min_zoom: u8,
81 pub max_zoom: u8,
82 pub min_lon_e7: i32,
86 pub min_lat_e7: i32,
87 pub max_lon_e7: i32,
88 pub max_lat_e7: i32,
89 pub t_min: i64,
92 pub t_max: i64,
93}
94
95impl PageDescriptor {
96 #[allow(clippy::too_many_arguments)]
100 pub fn overlaps(
101 &self,
102 zoom: u8,
103 q_min_lon_e7: i32,
104 q_min_lat_e7: i32,
105 q_max_lon_e7: i32,
106 q_max_lat_e7: i32,
107 t_start: i64,
108 t_end: i64,
109 ) -> bool {
110 zoom >= self.min_zoom
111 && zoom <= self.max_zoom
112 && self.min_lon_e7 <= q_max_lon_e7
113 && q_min_lon_e7 <= self.max_lon_e7
114 && self.min_lat_e7 <= q_max_lat_e7
115 && q_min_lat_e7 <= self.max_lat_e7
116 && self.t_max >= t_start
117 && self.t_min <= t_end
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct PagedRoot {
124 pub descriptor_kind: u8,
125 pub page_entries: u32,
127 pub pages: Vec<PageDescriptor>,
128}
129
130pub struct EncodedPagedDirectory {
133 pub bytes: Vec<u8>,
135 pub root_length: u64,
137 pub page_count: usize,
138 pub page_entries: usize,
139}
140
141#[inline]
142fn floor_e7(deg: f64) -> i32 {
143 (deg * 1e7).floor().clamp(i32::MIN as f64, i32::MAX as f64) as i32
144}
145
146#[inline]
147fn ceil_e7(deg: f64) -> i32 {
148 (deg * 1e7).ceil().clamp(i32::MIN as f64, i32::MAX as f64) as i32
149}
150
151fn put_u16(buf: &mut Vec<u8>, v: u16) {
152 buf.extend_from_slice(&v.to_le_bytes());
153}
154fn put_u32(buf: &mut Vec<u8>, v: u32) {
155 buf.extend_from_slice(&v.to_le_bytes());
156}
157fn put_u64(buf: &mut Vec<u8>, v: u64) {
158 buf.extend_from_slice(&v.to_le_bytes());
159}
160fn put_i32(buf: &mut Vec<u8>, v: i32) {
161 buf.extend_from_slice(&v.to_le_bytes());
162}
163fn put_i64(buf: &mut Vec<u8>, v: i64) {
164 buf.extend_from_slice(&v.to_le_bytes());
165}
166
167pub fn encode_root(page_entries: u32, descriptors: &[PageDescriptor]) -> Vec<u8> {
169 let mut buf = Vec::with_capacity(ROOT_HEADER_LEN + descriptors.len() * DESCRIPTOR_LEN);
170 buf.push(PAGED_ROOT_VERSION);
171 buf.push(DESCRIPTOR_GEO_BBOX);
172 put_u16(&mut buf, 0); put_u32(&mut buf, descriptors.len() as u32);
174 put_u32(&mut buf, page_entries);
175 for d in descriptors {
176 put_u64(&mut buf, d.rel_offset);
177 put_u32(&mut buf, d.length);
178 put_u32(&mut buf, d.entry_count);
179 buf.push(d.min_zoom);
180 buf.push(d.max_zoom);
181 put_u16(&mut buf, 0); put_i32(&mut buf, d.min_lon_e7);
183 put_i32(&mut buf, d.min_lat_e7);
184 put_i32(&mut buf, d.max_lon_e7);
185 put_i32(&mut buf, d.max_lat_e7);
186 put_i64(&mut buf, d.t_min);
187 put_i64(&mut buf, d.t_max);
188 }
189 buf
190}
191
192struct Reader<'a> {
193 bytes: &'a [u8],
194 pos: usize,
195}
196impl<'a> Reader<'a> {
197 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
198 let end = self.pos + n;
199 let slice = self
200 .bytes
201 .get(self.pos..end)
202 .ok_or_else(|| Error::InvalidArchive("paged root: truncated".into()))?;
203 self.pos = end;
204 Ok(slice)
205 }
206 fn u16(&mut self) -> Result<u16> {
207 Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
208 }
209 fn u32(&mut self) -> Result<u32> {
210 Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
211 }
212 fn u64(&mut self) -> Result<u64> {
213 Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
214 }
215 fn i32(&mut self) -> Result<i32> {
216 Ok(i32::from_le_bytes(self.take(4)?.try_into().unwrap()))
217 }
218 fn i64(&mut self) -> Result<i64> {
219 Ok(i64::from_le_bytes(self.take(8)?.try_into().unwrap()))
220 }
221}
222
223pub fn decode_root(bytes: &[u8]) -> Result<PagedRoot> {
225 let mut r = Reader { bytes, pos: 0 };
226 let version = *bytes
227 .first()
228 .ok_or_else(|| Error::InvalidArchive("paged root: empty".into()))?;
229 r.pos = 1;
230 if version != PAGED_ROOT_VERSION {
231 return Err(Error::InvalidArchive(format!(
232 "paged root: unsupported version {version} (expected {PAGED_ROOT_VERSION})"
233 )));
234 }
235 let descriptor_kind = *bytes.get(1).unwrap();
236 r.pos = 2;
237 if descriptor_kind != DESCRIPTOR_GEO_BBOX {
238 return Err(Error::InvalidArchive(format!(
239 "paged root: unsupported descriptor kind {descriptor_kind}"
240 )));
241 }
242 let _reserved = r.u16()?;
243 let page_count = r.u32()? as usize;
244 let page_entries = r.u32()?;
245 if bytes.len().saturating_sub(r.pos) < page_count.saturating_mul(DESCRIPTOR_LEN) {
249 return Err(Error::InvalidArchive(format!(
250 "paged root: header claims {page_count} pages but only {} bytes remain",
251 bytes.len().saturating_sub(r.pos)
252 )));
253 }
254 let mut pages = Vec::with_capacity(page_count);
255 for _ in 0..page_count {
256 let rel_offset = r.u64()?;
257 let length = r.u32()?;
258 let entry_count = r.u32()?;
259 let min_zoom = r.take(1)?[0];
260 let max_zoom = r.take(1)?[0];
261 let _res = r.u16()?;
262 let min_lon_e7 = r.i32()?;
263 let min_lat_e7 = r.i32()?;
264 let max_lon_e7 = r.i32()?;
265 let max_lat_e7 = r.i32()?;
266 let t_min = r.i64()?;
267 let t_max = r.i64()?;
268 pages.push(PageDescriptor {
269 rel_offset,
270 length,
271 entry_count,
272 min_zoom,
273 max_zoom,
274 min_lon_e7,
275 min_lat_e7,
276 max_lon_e7,
277 max_lat_e7,
278 t_min,
279 t_max,
280 });
281 }
282 Ok(PagedRoot {
283 descriptor_kind,
284 page_entries,
285 pages,
286 })
287}
288
289fn frame(raw: Vec<u8>, zstd: bool, level: i32) -> Result<Vec<u8>> {
290 if zstd {
291 compression::compress_zstd_with_dict_level(&raw, None, level)
292 } else {
293 Ok(raw)
294 }
295}
296
297fn unframe(frame: &[u8], zstd: bool) -> Result<Vec<u8>> {
298 if zstd {
299 compression::decompress_zstd_with_dict(frame, None)
300 } else {
301 Ok(frame.to_vec())
302 }
303}
304
305pub fn encode_paged_directory(
318 entries: &[TileEntry],
319 page_entries: usize,
320 zstd: bool,
321) -> Result<EncodedPagedDirectory> {
322 encode_paged_directory_level(entries, page_entries, zstd, compression::ZSTD_LEVEL)
323}
324
325pub fn encode_paged_directory_level(
329 entries: &[TileEntry],
330 page_entries: usize,
331 zstd: bool,
332 level: i32,
333) -> Result<EncodedPagedDirectory> {
334 let page_entries = page_entries.max(1);
335 let mut sorted: Vec<&TileEntry> = entries.iter().collect();
336 sorted.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start, e.temporal_bucket_ms));
337
338 let all_cover = !sorted.is_empty() && sorted.iter().all(|e| e.cover_t_min.is_some());
340
341 let mut descriptors: Vec<PageDescriptor> = Vec::new();
342 let mut leaf_frames: Vec<Vec<u8>> = Vec::new();
343 let mut rel_offset = 0u64;
344
345 for chunk in sorted.chunks(page_entries) {
346 let owned: Vec<TileEntry> = chunk
349 .iter()
350 .map(|&e| {
351 let mut c = e.clone();
352 if !all_cover {
353 c.cover_t_min = None;
354 }
355 c
356 })
357 .collect();
358 let raw = encode_directory(&owned);
359 let leaf = frame(raw, zstd, level)?;
360
361 let mut geo = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
362 let (mut zmin, mut zmax) = (u8::MAX, 0u8);
363 let (mut tmin, mut tmax) = (i64::MAX, i64::MIN);
364 for &e in chunk {
365 let b = tile_geo_bounds(e.zoom, e.x, e.y);
366 geo.0 = geo.0.min(b.0);
367 geo.1 = geo.1.min(b.1);
368 geo.2 = geo.2.max(b.2);
369 geo.3 = geo.3.max(b.3);
370 zmin = zmin.min(e.zoom);
371 zmax = zmax.max(e.zoom);
372 tmin = tmin.min(e.cover_t_min.unwrap_or(e.time_start));
373 tmax = tmax.max(e.time_end);
374 }
375
376 descriptors.push(PageDescriptor {
377 rel_offset,
378 length: leaf.len() as u32,
379 entry_count: chunk.len() as u32,
380 min_zoom: zmin,
381 max_zoom: zmax,
382 min_lon_e7: floor_e7(geo.0),
383 min_lat_e7: floor_e7(geo.1),
384 max_lon_e7: ceil_e7(geo.2),
385 max_lat_e7: ceil_e7(geo.3),
386 t_min: tmin,
387 t_max: tmax,
388 });
389 rel_offset += leaf.len() as u64;
390 leaf_frames.push(leaf);
391 }
392
393 let root_raw = encode_root(page_entries as u32, &descriptors);
394 let root_frame = frame(root_raw, zstd, level)?;
395 let root_length = root_frame.len() as u64;
396 let page_count = descriptors.len();
397
398 let mut bytes = root_frame;
399 for f in &leaf_frames {
400 bytes.extend_from_slice(f);
401 }
402
403 Ok(EncodedPagedDirectory {
404 bytes,
405 root_length,
406 page_count,
407 page_entries,
408 })
409}
410
411pub fn decode_paged_directory(
419 bytes: &[u8],
420 root_length: u64,
421 zstd: bool,
422) -> Result<Vec<TileEntry>> {
423 let rl = root_length as usize;
424 if rl > bytes.len() {
425 return Err(Error::InvalidArchive(format!(
426 "paged directory: rootLength {rl} exceeds object size {}",
427 bytes.len()
428 )));
429 }
430 let root_raw = unframe(&bytes[..rl], zstd)?;
431 let root = decode_root(&root_raw)?;
432 let mut entries = Vec::new();
433 for d in &root.pages {
434 let start = root_length.checked_add(d.rel_offset);
437 let end = start.and_then(|s| s.checked_add(d.length as u64));
438 let frame = match (start, end) {
439 (Some(s), Some(e)) if e <= bytes.len() as u64 => &bytes[s as usize..e as usize],
440 _ => {
441 return Err(Error::InvalidArchive(format!(
442 "paged directory: leaf range rootLength+{}..+{} exceeds object size {}",
443 d.rel_offset,
444 d.length,
445 bytes.len()
446 )))
447 }
448 };
449 let raw = unframe(frame, zstd)?;
450 let mut page = decode_directory(&raw)?;
451 if page.len() != d.entry_count as usize {
452 return Err(Error::InvalidArchive(format!(
453 "paged directory: leaf declared {} entries, decoded {}",
454 d.entry_count,
455 page.len()
456 )));
457 }
458 entries.append(&mut page);
459 }
460 Ok(entries)
461}
462
463pub fn verify_paged_structure(bytes: &[u8], root_length: u64, zstd: bool) -> Result<Vec<String>> {
478 const MAX_ISSUES: usize = 25;
479 let mut issues: Vec<String> = Vec::new();
480 let push = |issues: &mut Vec<String>, msg: String| {
481 if issues.len() < MAX_ISSUES {
482 issues.push(msg);
483 } else if issues.len() == MAX_ISSUES {
484 issues.push("… (further paged-structure issues truncated)".into());
485 }
486 };
487
488 let rl = root_length as usize;
489 if rl > bytes.len() {
490 return Ok(vec![format!(
491 "paged: rootLength {rl} exceeds object size {}",
492 bytes.len()
493 )]);
494 }
495 let root = decode_root(&unframe(&bytes[..rl], zstd)?)?;
496
497 let mut decoded_total = 0usize;
498 let mut declared_total = 0usize;
499 let mut prev_last: Option<(u8, u64, i64)> = None;
500 for (pi, d) in root.pages.iter().enumerate() {
501 declared_total += d.entry_count as usize;
502 let start = root_length.checked_add(d.rel_offset);
505 let end = start.and_then(|s| s.checked_add(d.length as u64));
506 let frame = match (start, end) {
507 (Some(s), Some(e)) if e <= bytes.len() as u64 => &bytes[s as usize..e as usize],
508 _ => {
509 push(
510 &mut issues,
511 format!(
512 "page {pi}: leaf range rootLength+{}..+{} exceeds object size {}",
513 d.rel_offset,
514 d.length,
515 bytes.len()
516 ),
517 );
518 continue;
519 }
520 };
521 let leaf = match unframe(frame, zstd).and_then(|raw| decode_directory(&raw)) {
522 Ok(l) => l,
523 Err(e) => {
524 push(&mut issues, format!("page {pi}: leaf decode failed: {e}"));
525 continue;
526 }
527 };
528 if leaf.len() != d.entry_count as usize {
529 push(
530 &mut issues,
531 format!("page {pi}: descriptor declares {} entries, leaf decoded {}", d.entry_count, leaf.len()),
532 );
533 }
534 decoded_total += leaf.len();
535
536 for e in &leaf {
537 if e.zoom < d.min_zoom || e.zoom > d.max_zoom {
538 push(
539 &mut issues,
540 format!("page {pi}: entry zoom {} outside descriptor [{}, {}]", e.zoom, d.min_zoom, d.max_zoom),
541 );
542 }
543 let b = tile_geo_bounds(e.zoom, e.x, e.y);
544 if floor_e7(b.0) < d.min_lon_e7
545 || floor_e7(b.1) < d.min_lat_e7
546 || ceil_e7(b.2) > d.max_lon_e7
547 || ceil_e7(b.3) > d.max_lat_e7
548 {
549 push(
550 &mut issues,
551 format!("page {pi}: descriptor bbox does not cover tile {}/{}/{}", e.zoom, e.x, e.y),
552 );
553 }
554 let lo = e.cover_t_min.unwrap_or(e.time_start);
555 if lo < d.t_min || e.time_end > d.t_max {
556 push(
557 &mut issues,
558 format!("page {pi}: descriptor t-bounds [{}, {}] do not cover entry [{}, {}]", d.t_min, d.t_max, lo, e.time_end),
559 );
560 }
561 }
562
563 if let (Some(first), Some(last)) = (leaf.first(), leaf.last()) {
565 let fk = (first.zoom, first.hilbert, first.time_start);
566 if let Some(pl) = prev_last {
567 if pl > fk {
568 push(
569 &mut issues,
570 format!("page {pi}: first key {fk:?} precedes previous page's last key {pl:?} (directory order violated)"),
571 );
572 }
573 }
574 prev_last = Some((last.zoom, last.hilbert, last.time_start));
575 }
576 }
577
578 if decoded_total != declared_total {
579 push(
580 &mut issues,
581 format!("paged: descriptors declare {declared_total} entries, leaves decoded {decoded_total}"),
582 );
583 }
584 Ok(issues)
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590
591 fn entry(z: u8, x: u32, y: u32, ts: i64, te: i64, cover: Option<i64>) -> TileEntry {
592 TileEntry {
593 zoom: z,
594 x,
595 y,
596 time_start: ts,
597 time_end: te,
598 pack_id: 0,
599 offset: (x as u64) * 64,
600 length: 50 + x,
601 uncompressed_size: 100 + x,
602 feature_count: x,
603 hilbert: crate::tile::TileId::new(z, x, y, 0).hilbert_index(),
604 crc32c: 0x1000 + x,
605 temporal_bucket_ms: Some(3_600_000),
606 cover_t_min: cover,
607 }
608 }
609
610 fn corpus(all_cover: bool) -> Vec<TileEntry> {
612 let mut v = Vec::new();
613 for z in [4u8, 8, 12] {
614 let n = 1u32 << z;
615 for i in 0..40u32 {
616 let x = (i * 7) % n;
617 let y = (i * 13) % n;
618 for b in 0..3i64 {
619 let ts = b * 3_600_000 + i as i64 * 1000;
620 let cover = if all_cover { Some(ts - 500) } else { None };
621 v.push(entry(z, x, y, ts, ts + 3_599_000, cover));
622 }
623 }
624 }
625 v
626 }
627
628 fn whole_load(entries: &[TileEntry]) -> Vec<TileEntry> {
630 decode_directory(&encode_directory(entries)).unwrap()
631 }
632
633 #[test]
634 fn paged_decode_equals_whole_load_all_cover() {
635 let c = corpus(true);
636 let want = whole_load(&c);
637 for pe in [1usize, 2, 7, 50, 4096, 100_000] {
638 for zstd in [true, false] {
639 let enc = encode_paged_directory(&c, pe, zstd).unwrap();
640 let got = decode_paged_directory(&enc.bytes, enc.root_length, zstd).unwrap();
641 assert_eq!(got, want, "page_entries={pe} zstd={zstd}");
642 }
643 }
644 }
645
646 #[test]
647 fn paged_decode_equals_whole_load_mixed_cover() {
648 let mut c = corpus(true);
652 c[0].cover_t_min = None; let want = whole_load(&c);
654 assert!(want.iter().all(|e| e.cover_t_min.is_none()));
655 for pe in [1usize, 3, 4096] {
656 let enc = encode_paged_directory(&c, pe, true).unwrap();
657 let got = decode_paged_directory(&enc.bytes, enc.root_length, true).unwrap();
658 assert_eq!(got, want, "page_entries={pe}");
659 assert!(got.iter().all(|e| e.cover_t_min.is_none()));
660 }
661 }
662
663 #[test]
664 fn empty_roundtrips() {
665 let enc = encode_paged_directory(&[], 4096, true).unwrap();
666 assert_eq!(enc.page_count, 0);
667 let got = decode_paged_directory(&enc.bytes, enc.root_length, true).unwrap();
668 assert!(got.is_empty());
669 }
670
671 #[test]
672 fn small_dataset_is_one_page() {
673 let c = corpus(true);
674 let enc = encode_paged_directory(&c, 100_000, true).unwrap();
675 assert_eq!(enc.page_count, 1);
676 }
677
678 #[test]
679 fn page_count_matches_chunking() {
680 let c = corpus(true); assert_eq!(c.len(), 360);
682 let enc = encode_paged_directory(&c, 100, true).unwrap();
683 assert_eq!(enc.page_count, 4); }
685
686 #[test]
687 fn descriptor_bounds_cover_their_entries() {
688 let c = corpus(true);
689 let enc = encode_paged_directory(&c, 37, true).unwrap();
690 let root = decode_root(&unframe(&enc.bytes[..enc.root_length as usize], true).unwrap())
691 .unwrap();
692 let mut sorted = c.clone();
694 sorted.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start, e.temporal_bucket_ms));
695 let mut total = 0u32;
696 for (page, chunk) in root.pages.iter().zip(sorted.chunks(37)) {
697 assert_eq!(page.entry_count as usize, chunk.len());
698 total += page.entry_count;
699 for e in chunk {
700 let b = tile_geo_bounds(e.zoom, e.x, e.y);
701 assert!(e.zoom >= page.min_zoom && e.zoom <= page.max_zoom);
702 assert!(floor_e7(b.0) >= page.min_lon_e7, "min_lon under-covered");
705 assert!(floor_e7(b.1) >= page.min_lat_e7, "min_lat under-covered");
706 assert!(ceil_e7(b.2) <= page.max_lon_e7, "max_lon under-covered");
707 assert!(ceil_e7(b.3) <= page.max_lat_e7, "max_lat under-covered");
708 let lo = e.cover_t_min.unwrap_or(e.time_start);
709 assert!(lo >= page.t_min && e.time_end <= page.t_max);
710 }
711 }
712 assert_eq!(total as usize, c.len());
713 }
714
715 #[test]
716 fn cross_page_key_order_is_monotonic() {
717 let c = corpus(true);
718 let got = decode_paged_directory(
719 &encode_paged_directory(&c, 29, true).unwrap().bytes,
720 encode_paged_directory(&c, 29, true).unwrap().root_length,
721 true,
722 )
723 .unwrap();
724 for w in got.windows(2) {
726 let a = (w[0].zoom, w[0].hilbert, w[0].time_start);
727 let b = (w[1].zoom, w[1].hilbert, w[1].time_start);
728 assert!(a <= b, "directory order violated across pages: {a:?} > {b:?}");
729 }
730 }
731
732 #[test]
733 fn root_encode_decode_roundtrips() {
734 let descs = vec![
735 PageDescriptor {
736 rel_offset: 0,
737 length: 1234,
738 entry_count: 4096,
739 min_zoom: 8,
740 max_zoom: 9,
741 min_lon_e7: -740_000_000,
742 min_lat_e7: 400_000_000,
743 max_lon_e7: -730_000_000,
744 max_lat_e7: 410_000_000,
745 t_min: 1_000,
746 t_max: 9_999,
747 },
748 PageDescriptor {
749 rel_offset: 1234,
750 length: 5678,
751 entry_count: 12,
752 min_zoom: 0,
753 max_zoom: 14,
754 min_lon_e7: -1_800_000_000,
755 min_lat_e7: -850_000_000,
756 max_lon_e7: 1_800_000_000,
757 max_lat_e7: 850_000_000,
758 t_min: i64::MIN + 1,
759 t_max: i64::MAX - 1,
760 },
761 ];
762 let raw = encode_root(4096, &descs);
763 let root = decode_root(&raw).unwrap();
764 assert_eq!(root.page_entries, 4096);
765 assert_eq!(root.pages, descs);
766 }
767
768 #[test]
769 fn overlaps_selects_the_right_pages() {
770 let a = PageDescriptor {
772 rel_offset: 0,
773 length: 1,
774 entry_count: 1,
775 min_zoom: 10,
776 max_zoom: 10,
777 min_lon_e7: -740_000_000,
778 min_lat_e7: 400_000_000,
779 max_lon_e7: -730_000_000,
780 max_lat_e7: 410_000_000,
781 t_min: 0,
782 t_max: 1000,
783 };
784 let b = PageDescriptor {
785 min_lon_e7: 1_000_000_000,
786 max_lon_e7: 1_010_000_000,
787 min_lat_e7: -100_000_000,
788 max_lat_e7: -90_000_000,
789 t_min: 5000,
790 t_max: 6000,
791 ..a.clone()
792 };
793 assert!(a.overlaps(10, -735_000_000, 405_000_000, -731_000_000, 408_000_000, 0, 1000));
795 assert!(!b.overlaps(10, -735_000_000, 405_000_000, -731_000_000, 408_000_000, 0, 1000));
796 assert!(!a.overlaps(9, -735_000_000, 405_000_000, -731_000_000, 408_000_000, 0, 1000));
798 assert!(!a.overlaps(10, -735_000_000, 405_000_000, -731_000_000, 408_000_000, 2000, 3000));
800 }
801
802 #[test]
803 fn verify_paged_structure_clean_then_detects_corruption() {
804 let c = corpus(true);
805 let enc = encode_paged_directory(&c, 19, true).unwrap();
806 let issues = verify_paged_structure(&enc.bytes, enc.root_length, true).unwrap();
808 assert!(issues.is_empty(), "clean build had issues: {issues:?}");
809
810 let mut sorted = c.clone();
814 sorted.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start, e.temporal_bucket_ms));
815 let root_raw = unframe(&enc.bytes[..enc.root_length as usize], true).unwrap();
816 let mut root = decode_root(&root_raw).unwrap();
817 root.pages[0].min_lon_e7 = 1_790_000_000;
819 root.pages[0].max_lon_e7 = 1_800_000_000;
820 root.pages[1].t_max = i64::MIN;
822 let bad_root = encode_root(root.page_entries, &root.pages);
823 let bad_root_frame = frame(bad_root, true, compression::ZSTD_LEVEL).unwrap();
824 let mut bad = bad_root_frame.clone();
826 bad.extend_from_slice(&enc.bytes[enc.root_length as usize..]);
827 let bad_issues =
828 verify_paged_structure(&bad, bad_root_frame.len() as u64, true).unwrap();
829 assert!(
830 bad_issues.iter().any(|s| s.contains("bbox does not cover")),
831 "expected a bbox-cover violation, got {bad_issues:?}"
832 );
833 assert!(
834 bad_issues.iter().any(|s| s.contains("t-bounds")),
835 "expected a t-bounds violation, got {bad_issues:?}"
836 );
837 }
838
839 #[test]
840 fn truncated_root_errors() {
841 let c = corpus(true);
842 let enc = encode_paged_directory(&c, 50, false).unwrap();
843 assert!(decode_paged_directory(&enc.bytes, enc.bytes.len() as u64 + 1, false).is_err());
845 let mut raw = encode_root(50, &[]);
847 raw[0] = 99;
848 assert!(decode_root(&raw).is_err());
849 }
850}