1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{BufWriter, Write};
4use std::path::Path;
5use std::sync::Mutex;
6
7use rosbag::{ChunkRecord, IndexRecord, MessageRecord, RosBag};
8
9use super::{IoError, LoadOptions, SliceSource};
10use crate::{EventStream, EventStreamBuilder};
11
12const DEFAULT_TOPIC: &str = "/davis/left/events";
13const EVENT_ARRAY_TYPE: &str = "dvs_msgs/EventArray";
14const EVENT_ARRAY_MD5: &str = "5e8beee5759d85e9f5a8c1f5ad1cd00b";
17const EVENT_ARRAY_DEF: &str =
19 "std_msgs/Header header\nuint32 height\nuint32 width\ndvs_msgs/Event[] events\n";
20const MESSAGE_EVENTS: usize = 1_000_000;
22const BAG_MAGIC: &[u8] = b"#ROSBAG V2.0\n";
24
25pub fn read_bag(path: impl AsRef<Path>, options: &LoadOptions) -> Result<EventStream, IoError> {
29 let topic = options.topic.as_deref().unwrap_or(DEFAULT_TOPIC);
30 let bag = RosBag::new(path).map_err(IoError::Io)?;
31
32 let mut wanted: HashMap<u32, bool> = HashMap::new();
33 let mut builder: Option<EventStreamBuilder> = None;
34
35 'outer: for record in bag.chunk_records() {
36 let ChunkRecord::Chunk(chunk) = record.map_err(map_bag_error)? else {
37 continue;
38 };
39 for message in chunk.messages() {
40 match message.map_err(map_bag_error)? {
41 MessageRecord::Connection(connection) => {
42 let matches = connection.topic == topic && connection.tp == EVENT_ARRAY_TYPE;
43 wanted.insert(connection.id, matches);
44 }
45 MessageRecord::MessageData(data) => {
46 if !wanted.get(&data.conn_id).copied().unwrap_or(false) {
47 continue;
48 }
49 let (width, height) = match options.sensor_size {
50 Some(size) => size,
51 None => read_event_array_header(data.data)?,
52 };
53 let builder = builder
54 .get_or_insert_with(|| EventStreamBuilder::new(width, height, 0.001));
55 let stopped = decode_event_array(data.data, &mut |x, y, t, p| {
56 builder.push(x, y, t, p);
57 options.max_events.is_some_and(|max| builder.len() >= max)
58 })?;
59 if stopped {
60 break 'outer;
61 }
62 }
63 }
64 }
65 }
66
67 builder.map(EventStreamBuilder::build).ok_or_else(|| {
68 IoError::Format(format!(
69 "no {EVENT_ARRAY_TYPE} messages found on topic {topic}"
70 ))
71 })
72}
73
74fn read_event_array_header(bytes: &[u8]) -> Result<(usize, usize), IoError> {
77 let mut reader = ByteReader::new(bytes);
78 reader.skip(4 + 8)?; let frame_id_len = reader.u32()? as usize;
80 reader.skip(frame_id_len)?; let height = reader.u32()? as usize;
82 let width = reader.u32()? as usize;
83 Ok((width, height))
84}
85
86fn decode_event_array(
90 bytes: &[u8],
91 on_event: &mut dyn FnMut(u16, u16, i64, bool) -> bool,
92) -> Result<bool, IoError> {
93 let mut reader = ByteReader::new(bytes);
94 reader.skip(4 + 8)?;
95 let frame_id_len = reader.u32()? as usize;
96 reader.skip(frame_id_len)?;
97 reader.skip(4 + 4)?; let count = reader.u32()? as usize;
99 for _ in 0..count {
100 let x = reader.u16()?;
101 let y = reader.u16()?;
102 let seconds = i64::from(reader.u32()?);
103 let nanoseconds = i64::from(reader.u32()?);
104 let polarity = reader.u8()? != 0;
105 if on_event(x, y, seconds * 1_000_000 + nanoseconds / 1000, polarity) {
106 return Ok(true);
107 }
108 }
109 Ok(false)
110}
111
112pub fn write_bag(
118 path: impl AsRef<Path>,
119 stream: &EventStream,
120 topic: Option<&str>,
121) -> Result<(), IoError> {
122 let topic = topic.unwrap_or(DEFAULT_TOPIC);
123 let conn_id: u32 = 0;
124 let ts = stream.ts();
125
126 let mut payload: Vec<u8> =
129 make_record(&connection_fields(conn_id, topic), &connection_data(topic));
130 let mut index_entries: Vec<(i64, u32)> = Vec::new();
131 let mut start = 0;
132 while start < stream.len() {
133 let end = (start + MESSAGE_EVENTS).min(stream.len());
134 let offset = payload.len() as u32;
135 let message = serialize_event_array(stream, start, end);
136 write_record(&mut payload, &message_fields(conn_id, ts[start]), &message)
137 .expect("writing to a Vec never fails");
138 index_entries.push((ts[start], offset));
139 start = end;
140 }
141 if stream.is_empty() {
142 let offset = payload.len() as u32;
145 let message = serialize_event_array(stream, 0, 0);
146 write_record(&mut payload, &message_fields(conn_id, 0), &message)
147 .expect("writing to a Vec never fails");
148 index_entries.push((0, offset));
149 }
150 let message_count = index_entries.len() as u32;
151 let (start_us, end_us) = match (ts.iter().min(), ts.iter().max()) {
152 (Some(&lo), Some(&hi)) => (lo, hi),
153 _ => (0, 0),
154 };
155
156 let bag_header_len = make_record(&bag_header_fields(0, 1, 1), &[]).len();
159 let chunk_pos = BAG_MAGIC.len() + bag_header_len;
160 let chunk_fields = chunk_fields(payload.len() as u32);
161 let mut index_data: Vec<u8> = Vec::with_capacity(index_entries.len() * 12);
162 for (time_us, offset) in &index_entries {
163 index_data.extend_from_slice(&ros_time(*time_us));
164 index_data.extend_from_slice(&offset.to_le_bytes());
165 }
166 let index_record = make_record(&index_data_fields(conn_id, message_count), &index_data);
167 let index_pos = chunk_pos + record_len(&chunk_fields, payload.len()) + index_record.len();
168
169 let mut writer = BufWriter::new(File::create(path).map_err(IoError::Io)?);
170 writer.write_all(BAG_MAGIC).map_err(IoError::Io)?;
171 write_record(&mut writer, &bag_header_fields(index_pos as u64, 1, 1), &[])
172 .map_err(IoError::Io)?;
173 write_record(&mut writer, &chunk_fields, &payload).map_err(IoError::Io)?;
174 writer.write_all(&index_record).map_err(IoError::Io)?;
175 write_record(
177 &mut writer,
178 &connection_fields(conn_id, topic),
179 &connection_data(topic),
180 )
181 .map_err(IoError::Io)?;
182 let chunk_info_data = {
183 let mut data = Vec::with_capacity(8);
184 data.extend_from_slice(&conn_id.to_le_bytes());
185 data.extend_from_slice(&message_count.to_le_bytes());
186 data
187 };
188 write_record(
189 &mut writer,
190 &chunk_info_fields(chunk_pos as u64, start_us, end_us),
191 &chunk_info_data,
192 )
193 .map_err(IoError::Io)?;
194 writer.flush().map_err(IoError::Io)
195}
196
197fn ros_time(t_us: i64) -> [u8; 8] {
200 let t_us = t_us.max(0);
201 let seconds = (t_us / 1_000_000) as u32;
202 let nanoseconds = ((t_us % 1_000_000) * 1000) as u32;
203 let mut bytes = [0u8; 8];
204 bytes[..4].copy_from_slice(&seconds.to_le_bytes());
205 bytes[4..].copy_from_slice(&nanoseconds.to_le_bytes());
206 bytes
207}
208
209fn serialize_event_array(stream: &EventStream, start: usize, end: usize) -> Vec<u8> {
211 let (width, height) = stream.sensor_size();
212 let (xs, ys, ts, ps) = (stream.xs(), stream.ys(), stream.ts(), stream.ps());
213 let stamp = ts.get(start).copied().unwrap_or(0);
214 let mut message = Vec::with_capacity(24 + (end - start) * 13);
215 message.extend_from_slice(&0u32.to_le_bytes()); message.extend_from_slice(&ros_time(stamp)); message.extend_from_slice(&0u32.to_le_bytes()); message.extend_from_slice(&(height as u32).to_le_bytes());
219 message.extend_from_slice(&(width as u32).to_le_bytes());
220 message.extend_from_slice(&((end - start) as u32).to_le_bytes());
221 for index in start..end {
222 message.extend_from_slice(&xs[index].to_le_bytes());
223 message.extend_from_slice(&ys[index].to_le_bytes());
224 message.extend_from_slice(&ros_time(ts[index]));
225 message.push(u8::from(ps[index]));
226 }
227 message
228}
229
230fn field(name: &str, value: &[u8]) -> Vec<u8> {
232 let mut bytes = Vec::with_capacity(4 + name.len() + 1 + value.len());
233 let body_len = (name.len() + 1 + value.len()) as u32;
234 bytes.extend_from_slice(&body_len.to_le_bytes());
235 bytes.extend_from_slice(name.as_bytes());
236 bytes.push(b'=');
237 bytes.extend_from_slice(value);
238 bytes
239}
240
241fn record_len(fields: &[Vec<u8>], data_len: usize) -> usize {
243 4 + fields.iter().map(Vec::len).sum::<usize>() + 4 + data_len
244}
245
246fn make_record(fields: &[Vec<u8>], data: &[u8]) -> Vec<u8> {
248 let mut record = Vec::with_capacity(record_len(fields, data.len()));
249 write_record(&mut record, fields, data).expect("writing to a Vec never fails");
250 record
251}
252
253fn write_record<W: Write>(writer: &mut W, fields: &[Vec<u8>], data: &[u8]) -> std::io::Result<()> {
255 let header_len: usize = fields.iter().map(Vec::len).sum();
256 writer.write_all(&(header_len as u32).to_le_bytes())?;
257 for field in fields {
258 writer.write_all(field)?;
259 }
260 writer.write_all(&(data.len() as u32).to_le_bytes())?;
261 writer.write_all(data)
262}
263
264fn bag_header_fields(index_pos: u64, conn_count: u32, chunk_count: u32) -> Vec<Vec<u8>> {
265 vec![
266 field("op", &[0x03]),
267 field("index_pos", &index_pos.to_le_bytes()),
268 field("conn_count", &conn_count.to_le_bytes()),
269 field("chunk_count", &chunk_count.to_le_bytes()),
270 ]
271}
272
273fn chunk_fields(uncompressed_size: u32) -> Vec<Vec<u8>> {
274 vec![
275 field("op", &[0x05]),
276 field("compression", b"none"),
277 field("size", &uncompressed_size.to_le_bytes()),
278 ]
279}
280
281fn connection_fields(conn_id: u32, topic: &str) -> Vec<Vec<u8>> {
282 vec![
283 field("op", &[0x07]),
284 field("conn", &conn_id.to_le_bytes()),
285 field("topic", topic.as_bytes()),
286 ]
287}
288
289fn connection_data(topic: &str) -> Vec<u8> {
290 let fields = [
291 field("topic", topic.as_bytes()),
292 field("type", EVENT_ARRAY_TYPE.as_bytes()),
293 field("md5sum", EVENT_ARRAY_MD5.as_bytes()),
294 field("message_definition", EVENT_ARRAY_DEF.as_bytes()),
295 ];
296 fields.concat()
297}
298
299fn message_fields(conn_id: u32, time_us: i64) -> Vec<Vec<u8>> {
300 vec![
301 field("op", &[0x02]),
302 field("conn", &conn_id.to_le_bytes()),
303 field("time", &ros_time(time_us)),
304 ]
305}
306
307fn index_data_fields(conn_id: u32, count: u32) -> Vec<Vec<u8>> {
308 vec![
309 field("op", &[0x04]),
310 field("ver", &1u32.to_le_bytes()),
311 field("conn", &conn_id.to_le_bytes()),
312 field("count", &count.to_le_bytes()),
313 ]
314}
315
316fn chunk_info_fields(chunk_pos: u64, start_us: i64, end_us: i64) -> Vec<Vec<u8>> {
317 vec![
318 field("op", &[0x06]),
319 field("ver", &1u32.to_le_bytes()),
320 field("chunk_pos", &chunk_pos.to_le_bytes()),
321 field("start_time", &ros_time(start_us)),
322 field("end_time", &ros_time(end_us)),
323 field("count", &1u32.to_le_bytes()),
324 ]
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
330struct ChunkMeta {
331 pos: u64,
332 start_us: i64,
333 end_us: i64,
334}
335
336pub struct BagSliceSource {
342 bag: RosBag,
343 conn_ids: Vec<u32>,
344 chunks: Vec<ChunkMeta>,
345 sensor: (usize, usize),
346 span_us: (i64, i64),
347 counts: Mutex<Option<Vec<usize>>>,
348}
349
350pub fn open_bag_slice(
354 path: impl AsRef<Path>,
355 options: &LoadOptions,
356) -> Result<BagSliceSource, IoError> {
357 let topic = options.topic.as_deref().unwrap_or(DEFAULT_TOPIC);
358 let bag = RosBag::new(path).map_err(IoError::Io)?;
359
360 let mut conn_ids: Vec<u32> = Vec::new();
361 for record in bag.index_records() {
362 if let IndexRecord::Connection(connection) = record.map_err(map_bag_error)? {
363 if connection.topic == topic && connection.tp == EVENT_ARRAY_TYPE {
364 conn_ids.push(connection.id);
365 }
366 }
367 }
368 if conn_ids.is_empty() {
369 return Err(IoError::Format(format!(
370 "no {EVENT_ARRAY_TYPE} connection on topic {topic}"
371 )));
372 }
373
374 let mut chunks: Vec<ChunkMeta> = Vec::new();
375 for record in bag.index_records() {
376 if let IndexRecord::ChunkInfo(info) = record.map_err(map_bag_error)? {
377 if info
378 .entries()
379 .any(|entry| conn_ids.contains(&entry.conn_id))
380 {
381 chunks.push(ChunkMeta {
382 pos: info.chunk_pos,
383 start_us: (info.start_time / 1000) as i64,
384 end_us: (info.end_time / 1000) as i64,
385 });
386 }
387 }
388 }
389 chunks.sort_by_key(|chunk| chunk.start_us);
390
391 let span_us = match (
392 chunks.iter().map(|c| c.start_us).min(),
393 chunks.iter().map(|c| c.end_us).max(),
394 ) {
395 (Some(lo), Some(hi)) => (lo, hi),
396 _ => (0, 0),
397 };
398 let sensor = match options.sensor_size {
399 Some(size) => size,
400 None => detect_sensor(&bag, &chunks, &conn_ids)?,
401 };
402
403 Ok(BagSliceSource {
404 bag,
405 conn_ids,
406 chunks,
407 sensor,
408 span_us,
409 counts: Mutex::new(None),
410 })
411}
412
413fn detect_sensor(
415 bag: &RosBag,
416 chunks: &[ChunkMeta],
417 conn_ids: &[u32],
418) -> Result<(usize, usize), IoError> {
419 let Some(first) = chunks.first() else {
420 return Ok((1, 1));
421 };
422 let mut iterator = bag.chunk_records();
423 iterator.seek(first.pos).map_err(map_bag_error)?;
424 if let Some(record) = iterator.next() {
425 if let ChunkRecord::Chunk(chunk) = record.map_err(map_bag_error)? {
426 for message in chunk.messages() {
427 if let MessageRecord::MessageData(data) = message.map_err(map_bag_error)? {
428 if conn_ids.contains(&data.conn_id) {
429 return read_event_array_header(data.data);
430 }
431 }
432 }
433 }
434 }
435 Ok((1, 1))
436}
437
438fn select_chunks(chunks: &[ChunkMeta], t0: i64, t1: i64) -> impl Iterator<Item = &ChunkMeta> {
440 chunks
441 .iter()
442 .filter(move |chunk| chunk.start_us < t1 && chunk.end_us >= t0)
443}
444
445fn locate_chunk(counts: &[usize], i: usize) -> usize {
448 counts
449 .partition_point(|&count| count <= i)
450 .saturating_sub(1)
451}
452
453impl BagSliceSource {
454 fn for_each_event(
457 &self,
458 chunk: &ChunkMeta,
459 on_event: &mut dyn FnMut(u16, u16, i64, bool) -> bool,
460 ) -> Result<(), IoError> {
461 let mut iterator = self.bag.chunk_records();
462 iterator.seek(chunk.pos).map_err(map_bag_error)?;
463 let Some(record) = iterator.next() else {
464 return Ok(());
465 };
466 let ChunkRecord::Chunk(decoded) = record.map_err(map_bag_error)? else {
467 return Ok(());
468 };
469 for message in decoded.messages() {
470 if let MessageRecord::MessageData(data) = message.map_err(map_bag_error)? {
471 if self.conn_ids.contains(&data.conn_id) && decode_event_array(data.data, on_event)?
472 {
473 break;
474 }
475 }
476 }
477 Ok(())
478 }
479
480 fn cumulative_counts(&self) -> Result<Vec<usize>, IoError> {
482 let mut guard = self.counts.lock().unwrap();
483 if let Some(counts) = guard.as_ref() {
484 return Ok(counts.clone());
485 }
486 let mut counts = Vec::with_capacity(self.chunks.len() + 1);
487 counts.push(0);
488 for chunk in &self.chunks {
489 let mut events = 0usize;
490 self.for_each_event(chunk, &mut |_, _, _, _| {
491 events += 1;
492 false
493 })?;
494 counts.push(counts.last().unwrap() + events);
495 }
496 *guard = Some(counts.clone());
497 Ok(counts)
498 }
499}
500
501impl SliceSource for BagSliceSource {
502 fn sensor_size(&self) -> (usize, usize) {
503 self.sensor
504 }
505
506 fn timestamp_scale_ms(&self) -> f64 {
507 0.001
508 }
509
510 fn n_events(&self) -> usize {
511 self.cumulative_counts()
513 .ok()
514 .and_then(|counts| counts.last().copied())
515 .unwrap_or(0)
516 }
517
518 fn time_span(&self) -> (i64, i64) {
519 self.span_us
520 }
521
522 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError> {
523 let mut builder = EventStreamBuilder::new(self.sensor.0, self.sensor.1, 0.001);
524 for chunk in select_chunks(&self.chunks, t0, t1) {
525 self.for_each_event(chunk, &mut |x, y, t, p| {
526 if t >= t1 {
527 return true; }
529 if t >= t0 {
530 builder.push(x, y, t, p);
531 }
532 false
533 })?;
534 }
535 Ok(builder.build())
536 }
537
538 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError> {
539 let counts = self.cumulative_counts()?;
540 let total = counts.last().copied().unwrap_or(0);
541 let i0 = i0.min(total);
542 let i1 = i1.clamp(i0, total);
543 let mut builder = EventStreamBuilder::new(self.sensor.0, self.sensor.1, 0.001);
544 if i0 == i1 {
545 return Ok(builder.build());
546 }
547 for (offset, chunk) in self
548 .chunks
549 .iter()
550 .enumerate()
551 .skip(locate_chunk(&counts, i0))
552 {
553 if counts[offset] >= i1 {
554 break;
555 }
556 let mut index = counts[offset];
557 self.for_each_event(chunk, &mut |x, y, t, p| {
558 if (i0..i1).contains(&index) {
559 builder.push(x, y, t, p);
560 }
561 index += 1;
562 index >= i1
563 })?;
564 }
565 Ok(builder.build())
566 }
567}
568
569fn map_bag_error(error: rosbag::Error) -> IoError {
570 IoError::Format(format!("rosbag: {error}"))
571}
572
573struct ByteReader<'a> {
575 bytes: &'a [u8],
576 position: usize,
577}
578
579impl<'a> ByteReader<'a> {
580 fn new(bytes: &'a [u8]) -> Self {
581 Self { bytes, position: 0 }
582 }
583
584 fn take(&mut self, count: usize) -> Result<&'a [u8], IoError> {
585 let end = self
586 .position
587 .checked_add(count)
588 .filter(|&end| end <= self.bytes.len())
589 .ok_or_else(|| IoError::Format("truncated dvs_msgs/EventArray message".to_owned()))?;
590 let slice = &self.bytes[self.position..end];
591 self.position = end;
592 Ok(slice)
593 }
594
595 fn skip(&mut self, count: usize) -> Result<(), IoError> {
596 self.take(count).map(|_| ())
597 }
598
599 fn u8(&mut self) -> Result<u8, IoError> {
600 Ok(self.take(1)?[0])
601 }
602
603 fn u16(&mut self) -> Result<u16, IoError> {
604 Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
605 }
606
607 fn u32(&mut self) -> Result<u32, IoError> {
608 Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::{locate_chunk, open_bag_slice, read_bag, select_chunks, write_bag, ChunkMeta};
615 use crate::io::{LoadOptions, SliceSource};
616 use crate::{EventStream, EventStreamBuilder};
617
618 fn temp_path(tag: &str) -> std::path::PathBuf {
619 let nanos = std::time::SystemTime::now()
620 .duration_since(std::time::UNIX_EPOCH)
621 .unwrap()
622 .as_nanos();
623 std::env::temp_dir().join(format!("eventcv_{tag}_{nanos}.bag"))
624 }
625
626 fn sample_stream() -> EventStream {
627 let mut builder = EventStreamBuilder::new(16, 12, 0.001);
628 for &(x, y, t, p) in &[
629 (0u16, 0u16, 5i64, true),
630 (3, 4, 1_000_001, false),
631 (15, 11, 2_500_000, true),
632 (7, 8, 2_500_000, false),
633 ] {
634 builder.push(x, y, t, p);
635 }
636 builder.build()
637 }
638
639 #[test]
640 fn bag_round_trips_through_the_reader() {
641 let stream = sample_stream();
642 let path = temp_path("bag_rt");
643 write_bag(&path, &stream, None).unwrap();
644
645 let loaded = read_bag(&path, &LoadOptions::default()).unwrap();
646 assert_eq!(loaded.sensor_size(), (16, 12));
647 assert_eq!(loaded.xs(), stream.xs());
648 assert_eq!(loaded.ys(), stream.ys());
649 assert_eq!(loaded.ts(), stream.ts()); assert_eq!(loaded.ps(), stream.ps());
651
652 let reader = open_bag_slice(&path, &LoadOptions::default()).unwrap();
654 assert_eq!(reader.sensor_size(), (16, 12));
655 assert_eq!(reader.n_events(), stream.len());
656 assert_eq!(reader.time_span(), (5, 2_500_000));
657 let window = reader.slice_time(1_000_000, 2_000_000).unwrap();
658 assert_eq!(window.ts(), &[1_000_001]);
659
660 std::fs::remove_file(&path).ok();
661 }
662
663 #[test]
664 fn bag_round_trips_an_empty_stream() {
665 let stream = EventStreamBuilder::new(8, 6, 0.001).build();
666 let path = temp_path("bag_empty");
667 write_bag(&path, &stream, Some("/cam/events")).unwrap();
668
669 let options = LoadOptions {
670 topic: Some("/cam/events".to_owned()),
671 ..LoadOptions::default()
672 };
673 let loaded = read_bag(&path, &options).unwrap();
674 assert!(loaded.is_empty());
675 assert_eq!(loaded.sensor_size(), (8, 6));
676
677 std::fs::remove_file(&path).ok();
678 }
679
680 fn meta(pos: u64, start_us: i64, end_us: i64) -> ChunkMeta {
681 ChunkMeta {
682 pos,
683 start_us,
684 end_us,
685 }
686 }
687
688 #[test]
689 fn select_chunks_picks_overlapping_windows() {
690 let chunks = [meta(0, 0, 100), meta(1, 100, 200), meta(2, 200, 300)];
691
692 let picked: Vec<u64> = select_chunks(&chunks, 150, 250).map(|c| c.pos).collect();
693 assert_eq!(picked, [1, 2]); let picked: Vec<u64> = select_chunks(&chunks, 0, 100).map(|c| c.pos).collect();
697 assert_eq!(picked, [0]);
698
699 assert!(select_chunks(&chunks, 1000, 2000).next().is_none());
700 }
701
702 #[test]
703 fn locate_chunk_finds_the_containing_chunk() {
704 let counts = [0usize, 10, 25, 40]; assert_eq!(locate_chunk(&counts, 0), 0);
707 assert_eq!(locate_chunk(&counts, 9), 0);
708 assert_eq!(locate_chunk(&counts, 10), 1);
709 assert_eq!(locate_chunk(&counts, 24), 1);
710 assert_eq!(locate_chunk(&counts, 39), 2);
711 }
712}