1use crate::archive::TileEntry;
50use crate::error::{Error, Result};
51
52pub const DIRECTORY_VERSION: u8 = 5;
58
59const COVER_SECTION_TMIN: u8 = 1;
66
67fn put_uvarint(buf: &mut Vec<u8>, mut v: u64) {
72 loop {
73 let byte = (v & 0x7f) as u8;
74 v >>= 7;
75 if v != 0 {
76 buf.push(byte | 0x80);
77 } else {
78 buf.push(byte);
79 break;
80 }
81 }
82}
83
84fn get_uvarint(buf: &[u8], pos: &mut usize) -> Result<u64> {
85 let mut result = 0u64;
86 let mut shift = 0u32;
87 loop {
88 let byte = *buf
89 .get(*pos)
90 .ok_or_else(|| Error::InvalidArchive("directory: truncated varint".into()))?;
91 *pos += 1;
92 result |= ((byte & 0x7f) as u64) << shift;
93 if byte & 0x80 == 0 {
94 break;
95 }
96 shift += 7;
97 if shift >= 64 {
98 return Err(Error::InvalidArchive("directory: varint exceeds 64 bits".into()));
99 }
100 }
101 Ok(result)
102}
103
104#[inline]
105fn zigzag(v: i64) -> u64 {
106 ((v << 1) ^ (v >> 63)) as u64
107}
108
109#[inline]
110fn unzigzag(v: u64) -> i64 {
111 ((v >> 1) as i64) ^ -((v & 1) as i64)
112}
113
114fn put_ivarint(buf: &mut Vec<u8>, v: i64) {
115 put_uvarint(buf, zigzag(v));
116}
117
118fn get_ivarint(buf: &[u8], pos: &mut usize) -> Result<i64> {
119 Ok(unzigzag(get_uvarint(buf, pos)?))
120}
121
122pub fn encode_directory(entries: &[TileEntry]) -> Vec<u8> {
136 let mut sorted: Vec<&TileEntry> = entries.iter().collect();
137 sorted.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start));
138 let n = sorted.len();
139
140 let mut runs: Vec<(usize, u32, u64, u32, u32, u32)> = Vec::new();
144 let mut i = 0;
145 while i < n {
146 let head = sorted[i];
147 let crc = head.crc32c;
148 let mut j = i + 1;
149 while j < n {
150 let e = sorted[j];
151 if e.pack_id == head.pack_id
152 && e.offset == head.offset
153 && e.length == head.length
154 && e.uncompressed_size == head.uncompressed_size
155 && e.crc32c == crc
156 {
157 j += 1;
158 } else {
159 break;
160 }
161 }
162 runs.push((
163 j - i,
164 head.pack_id,
165 head.offset,
166 head.length,
167 head.uncompressed_size,
168 crc,
169 ));
170 i = j;
171 }
172
173 let mut buf = Vec::with_capacity(n * 8 + runs.len() * 8 + 16);
174 buf.push(DIRECTORY_VERSION);
175 put_uvarint(&mut buf, n as u64);
176 put_uvarint(&mut buf, runs.len() as u64);
177
178 let mut prev_zoom = 0i64;
180 let mut prev_hilbert = 0i64;
181 let mut prev_x = 0i64;
182 let mut prev_y = 0i64;
183 let mut prev_t = 0i64;
184 for e in &sorted {
185 put_ivarint(&mut buf, (e.zoom as i64).wrapping_sub(prev_zoom));
186 prev_zoom = e.zoom as i64;
187 put_ivarint(&mut buf, (e.hilbert as i64).wrapping_sub(prev_hilbert));
188 prev_hilbert = e.hilbert as i64;
189 put_ivarint(&mut buf, (e.x as i64).wrapping_sub(prev_x));
190 prev_x = e.x as i64;
191 put_ivarint(&mut buf, (e.y as i64).wrapping_sub(prev_y));
192 prev_y = e.y as i64;
193 put_ivarint(&mut buf, e.time_start.wrapping_sub(prev_t));
194 prev_t = e.time_start;
195 put_ivarint(&mut buf, e.time_end.wrapping_sub(e.time_start));
197 put_uvarint(&mut buf, e.feature_count as u64);
198 match e.temporal_bucket_ms {
202 Some(v) => {
203 put_uvarint(&mut buf, 1);
204 put_uvarint(&mut buf, v);
205 }
206 None => put_uvarint(&mut buf, 0),
207 }
208 }
209
210 let mut expected_offset = 0u64;
216 let mut prev_pack_id = 0i64;
217 for (run_len, pack_id, offset, length, uncompressed, crc) in &runs {
218 put_uvarint(&mut buf, *run_len as u64);
219 let pid = *pack_id as i64;
222 if pid != prev_pack_id {
223 expected_offset = 0;
224 }
225 put_ivarint(&mut buf, pid.wrapping_sub(prev_pack_id));
226 prev_pack_id = pid;
227 if *offset == expected_offset {
231 put_uvarint(&mut buf, 0);
232 } else {
233 put_uvarint(&mut buf, 1);
234 put_uvarint(&mut buf, *offset);
235 }
236 put_uvarint(&mut buf, *length as u64);
237 put_uvarint(&mut buf, *uncompressed as u64);
238 buf.extend_from_slice(&crc.to_le_bytes());
239 expected_offset = offset.wrapping_add(*length as u64);
240 }
241
242 if n > 0 && sorted.iter().all(|e| e.cover_t_min.is_some()) {
248 buf.push(COVER_SECTION_TMIN);
249 for e in &sorted {
250 let delta = e.cover_t_min.unwrap().wrapping_sub(e.time_start);
253 put_ivarint(&mut buf, delta);
254 }
255 }
256
257 buf
258}
259
260const MIN_DIRECTORY_VERSION: u8 = 4;
270
271pub fn decode_directory(bytes: &[u8]) -> Result<Vec<TileEntry>> {
278 let mut pos = 0usize;
279 let version = *bytes
280 .first()
281 .ok_or_else(|| Error::InvalidArchive("directory: empty buffer".into()))?;
282 pos += 1;
283 if !(MIN_DIRECTORY_VERSION..=DIRECTORY_VERSION).contains(&version) {
284 return Err(Error::InvalidArchive(format!(
285 "directory: unsupported version {version} (expected {MIN_DIRECTORY_VERSION}..={DIRECTORY_VERSION})"
286 )));
287 }
288 let has_pack_id = version >= 5;
289 let n = get_uvarint(bytes, &mut pos)? as usize;
290 let run_count = get_uvarint(bytes, &mut pos)? as usize;
291
292 struct Key {
295 zoom: u8,
296 hilbert: u64,
297 x: u32,
298 y: u32,
299 time_start: i64,
300 time_end: i64,
301 feature_count: u32,
302 temporal_bucket_ms: Option<u64>,
303 }
304 let mut keys: Vec<Key> = Vec::with_capacity(n);
305 let mut prev_zoom = 0i64;
306 let mut prev_hilbert = 0i64;
307 let mut prev_x = 0i64;
308 let mut prev_y = 0i64;
309 let mut prev_t = 0i64;
310 for _ in 0..n {
311 prev_zoom = prev_zoom.wrapping_add(get_ivarint(bytes, &mut pos)?);
312 prev_hilbert = prev_hilbert.wrapping_add(get_ivarint(bytes, &mut pos)?);
313 prev_x = prev_x.wrapping_add(get_ivarint(bytes, &mut pos)?);
314 prev_y = prev_y.wrapping_add(get_ivarint(bytes, &mut pos)?);
315 prev_t = prev_t.wrapping_add(get_ivarint(bytes, &mut pos)?);
316 let duration = get_ivarint(bytes, &mut pos)?;
317 let feature_count_raw = get_uvarint(bytes, &mut pos)?;
318 let temporal_bucket_ms = if get_uvarint(bytes, &mut pos)? == 0 {
319 None
320 } else {
321 Some(get_uvarint(bytes, &mut pos)?)
322 };
323 if !(0..=u8::MAX as i64).contains(&prev_zoom) {
327 return Err(Error::InvalidArchive(format!(
328 "directory: zoom {prev_zoom} out of u8 range"
329 )));
330 }
331 if !(0..=u32::MAX as i64).contains(&prev_x) {
332 return Err(Error::InvalidArchive(format!(
333 "directory: x {prev_x} out of u32 range"
334 )));
335 }
336 if !(0..=u32::MAX as i64).contains(&prev_y) {
337 return Err(Error::InvalidArchive(format!(
338 "directory: y {prev_y} out of u32 range"
339 )));
340 }
341 if feature_count_raw > u32::MAX as u64 {
342 return Err(Error::InvalidArchive(format!(
343 "directory: feature_count {feature_count_raw} out of u32 range"
344 )));
345 }
346 keys.push(Key {
347 zoom: prev_zoom as u8,
348 hilbert: prev_hilbert as u64,
349 x: prev_x as u32,
350 y: prev_y as u32,
351 time_start: prev_t,
352 time_end: prev_t.wrapping_add(duration),
353 feature_count: feature_count_raw as u32,
354 temporal_bucket_ms,
355 });
356 }
357
358 let mut entries = Vec::with_capacity(n);
363 let mut cursor = 0usize;
364 let mut expected_offset = 0u64;
365 let mut prev_pack_id = 0i64;
366 for _ in 0..run_count {
367 let run_len = get_uvarint(bytes, &mut pos)? as usize;
368 let pid = if has_pack_id {
372 prev_pack_id.wrapping_add(get_ivarint(bytes, &mut pos)?)
373 } else {
374 0
375 };
376 if pid != prev_pack_id {
377 expected_offset = 0;
378 }
379 prev_pack_id = pid;
380 if !(0..=u32::MAX as i64).contains(&pid) {
381 return Err(Error::InvalidArchive(format!(
382 "directory: pack_id {pid} out of u32 range"
383 )));
384 }
385 let pack_id = pid as u32;
386 let offset = if get_uvarint(bytes, &mut pos)? == 0 {
387 expected_offset
388 } else {
389 get_uvarint(bytes, &mut pos)?
390 };
391 let length = get_uvarint(bytes, &mut pos)? as u32;
392 let uncompressed_size = get_uvarint(bytes, &mut pos)? as u32;
393 let crc = u32::from_le_bytes(
394 bytes
395 .get(pos..pos + 4)
396 .ok_or_else(|| Error::InvalidArchive("directory: truncated crc".into()))?
397 .try_into()
398 .unwrap(),
399 );
400 pos += 4;
401
402 if cursor + run_len > n {
403 return Err(Error::InvalidArchive(
404 "directory: run length exceeds entry count".into(),
405 ));
406 }
407 for _ in 0..run_len {
408 let k = &keys[cursor];
409 cursor += 1;
410 entries.push(TileEntry {
411 zoom: k.zoom,
412 x: k.x,
413 y: k.y,
414 time_start: k.time_start,
415 time_end: k.time_end,
416 pack_id,
417 offset,
418 length,
419 uncompressed_size,
420 feature_count: k.feature_count,
421 hilbert: k.hilbert,
422 crc32c: crc,
423 temporal_bucket_ms: k.temporal_bucket_ms,
424 cover_t_min: None,
425 });
426 }
427 expected_offset = offset.wrapping_add(length as u64);
428 }
429
430 if cursor != n {
431 return Err(Error::InvalidArchive(format!(
432 "directory: runs covered {cursor} entries, expected {n}"
433 )));
434 }
435
436 if pos < bytes.len() {
440 let tag = bytes[pos];
441 pos += 1;
442 if tag == COVER_SECTION_TMIN {
443 for e in entries.iter_mut() {
444 let delta = get_ivarint(bytes, &mut pos)?;
445 e.cover_t_min = Some(e.time_start.wrapping_add(delta));
446 }
447 }
448 }
449
450 Ok(entries)
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 fn entry(
458 zoom: u8,
459 x: u32,
460 y: u32,
461 hilbert: u64,
462 ts: i64,
463 te: i64,
464 offset: u64,
465 length: u32,
466 unc: u32,
467 fc: u32,
468 crc: u32,
469 tb: Option<u64>,
470 ) -> TileEntry {
471 TileEntry {
472 zoom,
473 x,
474 y,
475 time_start: ts,
476 time_end: te,
477 pack_id: 0,
478 offset,
479 length,
480 uncompressed_size: unc,
481 feature_count: fc,
482 hilbert,
483 crc32c: crc,
484 temporal_bucket_ms: tb,
485 cover_t_min: None,
486 }
487 }
488
489 #[allow(clippy::too_many_arguments)]
491 fn entry_pack(
492 pack_id: u32,
493 zoom: u8,
494 x: u32,
495 y: u32,
496 hilbert: u64,
497 ts: i64,
498 te: i64,
499 offset: u64,
500 length: u32,
501 unc: u32,
502 fc: u32,
503 crc: u32,
504 tb: Option<u64>,
505 ) -> TileEntry {
506 let mut e = entry(zoom, x, y, hilbert, ts, te, offset, length, unc, fc, crc, tb);
507 e.pack_id = pack_id;
508 e
509 }
510
511 #[test]
512 fn empty_roundtrips() {
513 let bytes = encode_directory(&[]);
514 let back = decode_directory(&bytes).unwrap();
515 assert!(back.is_empty());
516 }
517
518 #[test]
521 fn cover_t_min_section_roundtrips() {
522 let mut entries = Vec::new();
523 for i in 0..20u32 {
524 let mut e = entry(
525 10, i, i, i as u64, (i as i64) * 1000, (i as i64) * 1000 + 900,
526 64 + i as u64 * 50, 50, 100, i, 0x100 + i, Some(1000),
527 );
528 e.cover_t_min = Some(if i == 0 { -500 } else { (i as i64) * 1000 + 250 });
531 entries.push(e);
532 }
533 let bytes = encode_directory(&entries);
534 let back = decode_directory(&bytes).unwrap();
535 let mut expected = entries.clone();
536 expected.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start));
537 assert_eq!(back, expected);
538 assert!(back.iter().all(|e| e.cover_t_min.is_some()));
539 }
540
541 #[test]
544 fn absent_cover_section_is_backward_compatible() {
545 let e = entry(10, 5, 7, 42, 1000, 2000, 64, 128, 256, 3, 7, None);
546 let bytes = encode_directory(std::slice::from_ref(&e));
547 let back = decode_directory(&bytes).unwrap();
550 assert_eq!(back.len(), 1);
551 assert_eq!(back[0].cover_t_min, None);
552 }
553
554 #[test]
555 fn single_entry_roundtrips() {
556 let e = entry(10, 5, 7, 42, 1000, 2000, 64, 128, 256, 3, 0xDEAD_BEEF, Some(3_600_000));
557 let bytes = encode_directory(std::slice::from_ref(&e));
558 let back = decode_directory(&bytes).unwrap();
559 assert_eq!(back, vec![e]);
560 }
561
562 #[test]
565 fn contiguous_distinct_blobs_roundtrip() {
566 let mut entries = Vec::new();
567 let mut offset = 64u64;
568 for i in 0..50u32 {
569 let len = 100 + i;
570 entries.push(entry(
571 12,
572 i,
573 i,
574 i as u64, (i as i64) * 1000,
576 (i as i64) * 1000 + 500,
577 offset,
578 len,
579 len * 2,
580 i,
581 0x1000 + i,
582 None,
583 ));
584 offset += len as u64;
585 }
586 let bytes = encode_directory(&entries);
587 let back = decode_directory(&bytes).unwrap();
588 assert_eq!(back, entries);
589 }
590
591 #[test]
595 fn identical_across_time_collapses_to_one_run_and_roundtrips() {
596 let crc = 0xABCD_1234u32;
597 let mut entries = Vec::new();
598 for b in 0..100u64 {
600 entries.push(entry(
601 9,
602 3,
603 4,
604 77, (b as i64) * 3_600_000,
606 (b as i64) * 3_600_000 + 3_599_999,
607 4096, 512, 1024, 64,
611 crc, Some(3_600_000),
613 ));
614 }
615 let bytes = encode_directory(&entries);
616 let back = decode_directory(&bytes).unwrap();
617 assert_eq!(back, entries);
618
619 let mut distinct = entries.clone();
624 for (i, e) in distinct.iter_mut().enumerate() {
625 e.offset = 4096 + i as u64 * 512;
626 e.crc32c = 0x9000 + i as u32;
627 }
628 let distinct_bytes = encode_directory(&distinct);
629 eprintln!(
630 "static-cell RLE directory: {} bytes vs distinct-blob: {} bytes",
631 bytes.len(),
632 distinct_bytes.len()
633 );
634 assert!(
637 distinct_bytes.len() >= bytes.len() + 99 * 8,
638 "RLE should reclaim the per-blob columns: rle={}, distinct={}",
639 bytes.len(),
640 distinct_bytes.len()
641 );
642 }
643
644 #[test]
647 fn mixed_unsorted_corpus_roundtrips() {
648 let mut entries = Vec::new();
649 let mut off = 64u64;
650 for zoom in [4u8, 8, 12] {
651 for cell in 0..20u64 {
652 for t in 0..5i64 {
653 let len = 80 + (cell as u32 % 7);
654 let shared = t > 0 && t % 3 != 0;
656 let (offset, crc) = if shared {
657 (off, 0x5555)
658 } else {
659 off += len as u64;
660 (off, 0x6000 + cell as u32 + t as u32)
661 };
662 entries.push(entry(
663 zoom,
664 cell as u32,
665 (cell * 2) as u32,
666 cell * 10 + zoom as u64, t * 1000 + cell as i64,
668 t * 1000 + cell as i64 + 250,
669 offset,
670 len,
671 len * 3,
672 (cell + t as u64) as u32,
673 crc,
674 if zoom == 4 { Some(86_400_000) } else { None },
675 ));
676 }
677 }
678 }
679 entries.reverse();
681
682 let bytes = encode_directory(&entries);
683 let back = decode_directory(&bytes).unwrap();
684
685 let mut expected = entries.clone();
687 expected.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start));
688 assert_eq!(back, expected);
689 }
690
691 #[test]
692 fn negative_and_extreme_times_roundtrip() {
693 let entries = vec![
694 entry(0, 0, 0, 0, i64::MIN + 1, i64::MIN + 10, 64, 8, 16, 1, 1, None),
695 entry(0, 0, 0, 0, -5000, -1000, 72, 8, 16, 1, 2, Some(1)),
696 entry(0, 0, 0, 0, 0, 0, 80, 8, 16, 1, 3, None),
697 entry(0, 0, 0, 0, i64::MAX - 10, i64::MAX, 88, 8, 16, 1, 4, None),
698 ];
699 let bytes = encode_directory(&entries);
700 let back = decode_directory(&bytes).unwrap();
701 assert_eq!(back, entries);
702 }
703
704 #[test]
705 fn truncated_buffer_errors() {
706 let e = entry(10, 5, 7, 42, 1000, 2000, 64, 128, 256, 3, 7, None);
707 let bytes = encode_directory(std::slice::from_ref(&e));
708 let truncated = &bytes[..bytes.len() - 2];
710 assert!(decode_directory(truncated).is_err());
711 }
712
713 #[test]
714 fn wrong_version_errors() {
715 let mut bytes = encode_directory(&[]);
716 bytes[0] = 99;
717 assert!(decode_directory(&bytes).is_err());
718 }
719
720 #[test]
723 fn decode_rejects_out_of_range_columns() {
724 let mut buf = Vec::new();
725 buf.push(DIRECTORY_VERSION);
726 put_uvarint(&mut buf, 1); put_uvarint(&mut buf, 1); put_ivarint(&mut buf, 300); put_ivarint(&mut buf, 0); put_ivarint(&mut buf, 0); put_ivarint(&mut buf, 0); put_ivarint(&mut buf, 0); put_ivarint(&mut buf, 0); put_uvarint(&mut buf, 0); put_uvarint(&mut buf, 0); put_uvarint(&mut buf, 1); put_ivarint(&mut buf, 0); put_uvarint(&mut buf, 0); put_uvarint(&mut buf, 0); put_uvarint(&mut buf, 0); buf.extend_from_slice(&0u32.to_le_bytes()); assert!(decode_directory(&buf).is_err());
743 }
744
745 #[test]
748 fn u64_max_offset_and_bucket_roundtrip() {
749 let entries = vec![
750 entry(5, 1, 1, 10, 0, 100, u64::MAX, 8, 16, 1, 7, Some(u64::MAX)),
751 entry(5, 2, 2, 11, 0, 100, 64, 8, 16, 1, 8, Some(3_600_000)),
752 entry(5, 3, 3, 12, 0, 100, 0, 8, 16, 1, 9, None),
753 ];
754 let bytes = encode_directory(&entries);
755 let back = decode_directory(&bytes).unwrap();
756 assert_eq!(back, entries);
757 }
758
759 #[test]
763 fn multi_pack_offsets_are_pack_relative_and_roundtrip() {
764 let mut entries = Vec::new();
765 for pack_id in 0..3u32 {
768 let mut off = 0u64;
769 for i in 0..6u32 {
770 let hil = (pack_id as u64) * 100 + i as u64; let len = 40 + i;
772 entries.push(entry_pack(
773 pack_id,
774 9,
775 pack_id * 10 + i,
776 i,
777 hil,
778 (i as i64) * 1000,
779 (i as i64) * 1000 + 500,
780 off,
781 len,
782 len * 2,
783 i,
784 0x2000 + pack_id * 100 + i,
785 Some(3_600_000),
786 ));
787 off += len as u64;
788 }
789 }
790 let bytes = encode_directory(&entries);
791 let back = decode_directory(&bytes).unwrap();
792 let mut expected = entries.clone();
793 expected.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start));
794 assert_eq!(back, expected);
795 let packs: std::collections::BTreeSet<u32> = back.iter().map(|e| e.pack_id).collect();
798 assert_eq!(packs, [0u32, 1, 2].into_iter().collect());
799 for p in 0..3u32 {
800 assert!(back.iter().any(|e| e.pack_id == p && e.offset == 0));
801 }
802 }
803
804 #[test]
808 fn rle_collapses_within_pack_but_not_across_packs() {
809 let crc = 0x55AA_55AAu32;
810 let mut entries = Vec::new();
811 for b in 0..50u64 {
813 entries.push(entry_pack(
814 0, 9, 3, 4, 77,
815 (b as i64) * 3_600_000,
816 (b as i64) * 3_600_000 + 3_599_999,
817 4096, 512, 1024, 64, crc, Some(3_600_000),
818 ));
819 }
820 for b in 0..50u64 {
823 entries.push(entry_pack(
824 1, 9, 5, 6, 99,
825 (b as i64) * 3_600_000,
826 (b as i64) * 3_600_000 + 3_599_999,
827 4096, 512, 1024, 64, crc, Some(3_600_000),
828 ));
829 }
830 let bytes = encode_directory(&entries);
831 let back = decode_directory(&bytes).unwrap();
832 let mut expected = entries.clone();
833 expected.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start));
834 assert_eq!(back, expected);
835 assert!(back.iter().any(|e| e.pack_id == 0));
836 assert!(back.iter().any(|e| e.pack_id == 1));
837 }
838
839 #[test]
841 fn cover_section_roundtrips_with_pack_ids() {
842 let mut entries = Vec::new();
843 for i in 0..12u32 {
844 let pack_id = i / 4; let mut e = entry_pack(
846 pack_id, 10, i, i, i as u64,
847 (i as i64) * 1000, (i as i64) * 1000 + 900,
848 (i % 4) as u64 * 50, 50, 100, i, 0x300 + i, Some(1000),
849 );
850 e.cover_t_min = Some((i as i64) * 1000 + 250);
851 entries.push(e);
852 }
853 let bytes = encode_directory(&entries);
854 let back = decode_directory(&bytes).unwrap();
855 let mut expected = entries.clone();
856 expected.sort_by_key(|e| (e.zoom, e.hilbert, e.time_start));
857 assert_eq!(back, expected);
858 assert!(back.iter().all(|e| e.cover_t_min.is_some()));
859 let packs: std::collections::BTreeSet<u32> = back.iter().map(|e| e.pack_id).collect();
860 assert_eq!(packs, [0u32, 1, 2].into_iter().collect());
861 }
862}