1use super::*;
2use std::collections::{HashMap, HashSet, VecDeque};
3
4const DIRECTORY_UNIQUE_CACHE_CAPACITY: usize = 8;
5
6pub struct DirectoryReader {
7 files: Vec<FileReader>,
8 index: usize,
9 pending_realtime_seek: Option<u64>,
10 realtime_seek_bound: Option<(u64, Direction)>,
11 candidates: Vec<Option<DirectoryCandidate>>,
12 current_key: Option<DirectoryEntryKey>,
13 direction: Option<Direction>,
14 boot_newest: HashMap<[u8; 16], DirectoryBootNewest>,
15 unique_cache: HashMap<DirectoryUniqueCacheKey, DirectoryUniqueCacheEntry>,
16 unique_cache_order: VecDeque<DirectoryUniqueCacheKey>,
17 unique_state: Option<DirectoryUniqueState>,
18 #[cfg(test)]
19 unique_cache_builds: usize,
20 pub(super) non_overlapping: bool,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24struct DirectoryUniqueCacheKey {
25 field_name: String,
26 files: Vec<DirectoryUniqueFileSignature>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30struct DirectoryUniqueFileSignature {
31 file_id: [u8; 16],
32 n_objects: u64,
33 n_entries: u64,
34 n_data: u64,
35 n_fields: u64,
36 head_entry_seqnum: u64,
37 tail_entry_seqnum: u64,
38 head_entry_realtime: u64,
39 tail_entry_realtime: u64,
40 tail_entry_monotonic: u64,
41 tail_entry_boot_id: [u8; 16],
42}
43
44#[derive(Debug, Clone)]
45struct DirectoryUniqueCacheEntry {
46 payloads: Vec<Vec<u8>>,
47}
48
49#[derive(Debug, Clone)]
50struct DirectoryUniqueState {
51 key: DirectoryUniqueCacheKey,
52 index: usize,
53}
54
55#[derive(Debug, Clone, Copy)]
56struct DirectoryCandidate {
57 reader_index: usize,
58 key: DirectoryEntryKey,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub(super) struct DirectoryEntryKey {
63 pub(super) seqnum_id: [u8; 16],
64 pub(super) seqnum: u64,
65 pub(super) boot_id: [u8; 16],
66 pub(super) monotonic: u64,
67 pub(super) realtime: u64,
68 pub(super) xor_hash: u64,
69}
70
71#[derive(Debug, Clone, Copy)]
72struct DirectoryBootNewest {
73 machine_id: [u8; 16],
74 monotonic: u64,
75 realtime: u64,
76}
77
78impl DirectoryReader {
79 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
80 Self::open_with_options(path, ReaderOptions::default())
81 }
82
83 pub fn open_with_options(path: impl AsRef<Path>, options: ReaderOptions) -> Result<Self> {
84 let path = path.as_ref();
85 if !path.is_dir() {
86 return Err(SdkError::InvalidPath(format!(
87 "not a directory: {}",
88 path.display()
89 )));
90 }
91
92 let mut files = Vec::new();
93 for file_path in collect_journal_files(path)? {
94 if let Ok(reader) = FileReader::open_with_options(&file_path, options) {
95 files.push(reader);
96 }
97 }
98
99 Self::from_readers(files, true)
100 }
101
102 pub fn open_files<I, P>(paths: I) -> Result<Self>
103 where
104 I: IntoIterator<Item = P>,
105 P: AsRef<Path>,
106 {
107 Self::open_files_with_options(paths, ReaderOptions::default())
108 }
109
110 pub fn open_files_with_options<I, P>(paths: I, options: ReaderOptions) -> Result<Self>
111 where
112 I: IntoIterator<Item = P>,
113 P: AsRef<Path>,
114 {
115 let mut files = Vec::new();
116 for path in paths {
117 let path = path.as_ref();
118 if !path.is_file() || !is_journal_file_name(path) {
119 return Err(SdkError::InvalidPath(format!(
120 "not a journal file: {}",
121 path.display()
122 )));
123 }
124 files.push(FileReader::open_with_options(path, options)?);
125 }
126
127 Self::from_readers(files, false)
128 }
129
130 fn from_readers(mut files: Vec<FileReader>, allow_empty: bool) -> Result<Self> {
131 if files.is_empty() && !allow_empty {
132 return Err(SdkError::InvalidPath(
133 "no readable journal files".to_string(),
134 ));
135 }
136
137 files.sort_by_key(FileReader::header_realtime_start);
138 let boot_newest = build_directory_boot_newest(&files);
139 let non_overlapping = directory_files_non_overlapping(&files);
140 let candidates = vec![None; files.len()];
141 Ok(Self {
142 files,
143 index: usize::MAX,
144 pending_realtime_seek: None,
145 realtime_seek_bound: None,
146 candidates,
147 current_key: None,
148 direction: None,
149 boot_newest,
150 unique_cache: HashMap::new(),
151 unique_cache_order: VecDeque::new(),
152 unique_state: None,
153 #[cfg(test)]
154 unique_cache_builds: 0,
155 non_overlapping,
156 })
157 }
158
159 pub fn seek_head(&mut self) {
160 self.pending_realtime_seek = None;
161 self.realtime_seek_bound = None;
162 self.index = usize::MAX;
163 self.current_key = None;
164 self.direction = None;
165 self.reset_candidates();
166 for reader in &mut self.files {
167 reader.seek_head();
168 }
169 }
170
171 pub fn seek_tail(&mut self) {
172 self.pending_realtime_seek = None;
173 self.realtime_seek_bound = None;
174 self.index = usize::MAX;
175 self.current_key = None;
176 self.direction = None;
177 self.reset_candidates();
178 for reader in &mut self.files {
179 reader.seek_tail();
180 }
181 }
182
183 pub fn seek_realtime(&mut self, usec: u64) {
184 self.pending_realtime_seek = Some(usec);
185 self.realtime_seek_bound = None;
186 self.index = usize::MAX;
187 self.current_key = None;
188 self.direction = None;
189 self.reset_candidates();
190 }
191
192 pub fn next(&mut self) -> Result<bool> {
193 self.step_merged(Direction::Forward)
194 }
195
196 pub fn previous(&mut self) -> Result<bool> {
197 self.step_merged(Direction::Backward)
198 }
199
200 fn step_merged(&mut self, direction: Direction) -> Result<bool> {
201 if self.can_step_sequential(direction) {
202 return self.step_sequential(direction);
203 }
204
205 self.prepare_merge_direction(direction);
206
207 let mut best: Option<DirectoryCandidate> = None;
208 for idx in 0..self.files.len() {
209 self.fill_candidate(idx, direction)?;
210 let Some(candidate) = self.candidates[idx] else {
211 continue;
212 };
213 let replace = match best {
214 None => true,
215 Some(current) => {
216 let cmp = self.compare_entry_keys(candidate.key, current.key);
217 (direction == Direction::Forward && cmp < 0)
218 || (direction == Direction::Backward && cmp > 0)
219 }
220 };
221 if replace {
222 best = Some(candidate);
223 }
224 }
225
226 let Some(best) = best else {
227 self.index = usize::MAX;
228 self.realtime_seek_bound = None;
229 return Ok(false);
230 };
231
232 self.index = best.reader_index;
233 self.current_key = Some(best.key);
234 self.candidates[best.reader_index] = None;
235 self.realtime_seek_bound = None;
236 Ok(true)
237 }
238
239 fn prepare_merge_direction(&mut self, direction: Direction) {
240 if let Some(usec) = self.pending_realtime_seek.take() {
241 for reader in &mut self.files {
242 reader.seek_realtime(usec);
243 }
244 self.reset_candidates();
245 self.realtime_seek_bound = Some((usec, direction));
246 self.direction = Some(direction);
247 return;
248 }
249
250 if self.direction == Some(direction) {
251 return;
252 }
253
254 if let Some(current) = self.current_key {
255 for reader in &mut self.files {
256 reader.seek_realtime(current.realtime);
257 }
258 } else if direction == Direction::Forward {
259 for reader in &mut self.files {
260 reader.seek_head();
261 }
262 } else {
263 for reader in &mut self.files {
264 reader.seek_tail();
265 }
266 }
267
268 self.reset_candidates();
269 self.direction = Some(direction);
270 }
271
272 fn fill_candidate(&mut self, reader_index: usize, direction: Direction) -> Result<()> {
273 if self.candidates[reader_index].is_some() {
274 return Ok(());
275 }
276
277 loop {
278 if !self.advance_candidate_reader(reader_index, direction)? {
279 return Ok(());
280 }
281 let key = self.files[reader_index].current_directory_entry_key()?;
282 if !self.candidate_matches_realtime_bound(key) {
283 continue;
284 }
285 if !self.candidate_is_after_current(key, direction) {
286 continue;
287 }
288
289 self.candidates[reader_index] = Some(DirectoryCandidate { reader_index, key });
290 return Ok(());
291 }
292 }
293
294 fn advance_candidate_reader(
295 &mut self,
296 reader_index: usize,
297 direction: Direction,
298 ) -> Result<bool> {
299 match direction {
300 Direction::Forward => self.files[reader_index].next(),
301 Direction::Backward => self.files[reader_index].previous(),
302 }
303 }
304
305 fn candidate_matches_realtime_bound(&self, key: DirectoryEntryKey) -> bool {
306 let Some((usec, seek_direction)) = self.realtime_seek_bound else {
307 return true;
308 };
309 match seek_direction {
310 Direction::Forward => key.realtime >= usec,
311 Direction::Backward => key.realtime <= usec,
312 }
313 }
314
315 fn candidate_is_after_current(&self, key: DirectoryEntryKey, direction: Direction) -> bool {
316 let Some(current) = self.current_key else {
317 return true;
318 };
319 let cmp = self.compare_entry_keys(key, current);
320 match direction {
321 Direction::Forward => cmp > 0,
322 Direction::Backward => cmp < 0,
323 }
324 }
325
326 fn compare_entry_keys(&self, a: DirectoryEntryKey, b: DirectoryEntryKey) -> i8 {
327 if a == b {
328 return 0;
329 }
330
331 if a.seqnum_id == b.seqnum_id {
332 let cmp = cmp_u64(a.seqnum, b.seqnum);
333 if cmp != 0 {
334 return cmp;
335 }
336 }
337
338 if a.boot_id == b.boot_id {
339 let cmp = cmp_u64(a.monotonic, b.monotonic);
340 if cmp != 0 {
341 return cmp;
342 }
343 } else {
344 let cmp = self.compare_boot_ids(a.boot_id, b.boot_id);
345 if cmp != 0 {
346 return cmp;
347 }
348 }
349
350 let cmp = cmp_u64(a.realtime, b.realtime);
351 if cmp != 0 {
352 return cmp;
353 }
354 cmp_u64(a.xor_hash, b.xor_hash)
355 }
356
357 fn compare_boot_ids(&self, a: [u8; 16], b: [u8; 16]) -> i8 {
358 let Some(a_newest) = self.boot_newest.get(&a) else {
359 return 0;
360 };
361 let Some(b_newest) = self.boot_newest.get(&b) else {
362 return 0;
363 };
364 if a_newest.machine_id != b_newest.machine_id {
365 return 0;
366 }
367 cmp_u64(a_newest.realtime, b_newest.realtime)
368 }
369
370 fn reset_candidates(&mut self) {
371 if self.candidates.len() != self.files.len() {
372 self.candidates = vec![None; self.files.len()];
373 return;
374 }
375 for candidate in &mut self.candidates {
376 *candidate = None;
377 }
378 }
379
380 pub fn get_entry(&mut self) -> Result<Entry> {
381 if self.index >= self.files.len() {
382 return Err(SdkError::NoEntry);
383 }
384 self.files[self.index].get_entry()
385 }
386
387 pub fn visit_entry_payloads<F>(&mut self, visitor: F) -> Result<()>
388 where
389 F: FnMut(&[u8]) -> Result<()>,
390 {
391 if self.index >= self.files.len() {
392 return Err(SdkError::NoEntry);
393 }
394 self.files[self.index].visit_entry_payloads(visitor)
395 }
396
397 pub fn clear_entry_data_state(&mut self) {
398 if self.index < self.files.len() {
399 self.files[self.index].clear_entry_data_state();
400 }
401 }
402
403 pub fn entry_data_restart(&mut self) -> Result<()> {
404 if self.index >= self.files.len() {
405 return Err(SdkError::NoEntry);
406 }
407 self.files[self.index].entry_data_restart()
408 }
409
410 pub fn enumerate_entry_payload(&mut self) -> Result<Option<&[u8]>> {
411 if self.index >= self.files.len() {
412 return Err(SdkError::NoEntry);
413 }
414 self.files[self.index].enumerate_entry_payload()
415 }
416
417 pub fn collect_entry_payloads(&mut self, payloads: &mut Vec<Vec<u8>>) -> Result<()> {
418 if self.index >= self.files.len() {
419 return Err(SdkError::NoEntry);
420 }
421 self.files[self.index].collect_entry_payloads(payloads)
422 }
423
424 pub fn get_entry_payload(&mut self, field: &[u8]) -> Result<Option<Vec<u8>>> {
425 if self.index >= self.files.len() {
426 return Err(SdkError::NoEntry);
427 }
428 self.files[self.index].get_entry_payload(field)
429 }
430
431 pub fn get_realtime_usec(&self) -> Result<u64> {
432 if self.index >= self.files.len() {
433 return Err(SdkError::NoEntry);
434 }
435 self.files[self.index].get_realtime_usec()
436 }
437
438 pub fn get_seqnum(&self) -> Result<(u64, [u8; 16])> {
439 if self.index >= self.files.len() {
440 return Err(SdkError::NoEntry);
441 }
442 if let Some(key) = self.current_key {
443 return Ok((key.seqnum, key.seqnum_id));
444 }
445 self.files[self.index].get_seqnum()
446 }
447
448 pub fn get_monotonic_usec(&self) -> Result<(u64, [u8; 16])> {
449 if self.index >= self.files.len() {
450 return Err(SdkError::NoEntry);
451 }
452 if let Some(key) = self.current_key {
453 return Ok((key.monotonic, key.boot_id));
454 }
455 self.files[self.index].get_monotonic_usec()
456 }
457
458 pub fn get_cursor(&self) -> Result<String> {
459 if self.index >= self.files.len() {
460 return Err(SdkError::NoEntry);
461 }
462 self.files[self.index].get_cursor()
463 }
464
465 pub fn test_cursor(&self, cursor: &str) -> Result<bool> {
466 if self.index >= self.files.len() {
467 return Ok(false);
468 }
469 self.files[self.index].test_cursor(cursor)
470 }
471
472 pub fn seek_cursor(&mut self, cursor: &str) -> Result<()> {
473 let want = parse::parse_cursor_location(cursor, true)
474 .map_err(|err| SdkError::InvalidCursor(err.to_string()))?;
475 if want.realtime_set {
476 self.seek_realtime(want.realtime);
477 } else {
478 self.seek_head();
479 }
480 while self.next()? {
481 let current_cursor = self.get_cursor()?;
482 let got = parse::parse_cursor_location(¤t_cursor, false)
483 .map_err(|err| SdkError::InvalidCursor(err.to_string()))?;
484 if parse::cursor_location_at_or_after(&got, &want) {
485 return Ok(());
486 }
487 }
488 self.seek_tail();
489 Ok(())
490 }
491
492 pub fn enumerate_fields(&mut self) -> Result<Vec<String>> {
493 let mut fields = HashSet::new();
494 for reader in &mut self.files {
495 for field in reader.enumerate_fields()? {
496 fields.insert(field);
497 }
498 }
499 let mut out: Vec<_> = fields.into_iter().collect();
500 out.sort();
501 Ok(out)
502 }
503
504 pub fn query_unique(&mut self, field_name: &str) -> Result<Vec<Vec<u8>>> {
505 let mut out = Vec::new();
506 self.visit_unique_values(field_name, |value| {
507 out.push(value.to_vec());
508 Ok(())
509 })?;
510 Ok(out)
511 }
512
513 pub fn visit_unique_values<F>(&mut self, field_name: &str, mut visitor: F) -> Result<()>
514 where
515 F: FnMut(&[u8]) -> Result<()>,
516 {
517 let key = self.ensure_unique_cache(field_name)?;
518 let Some(entry) = self.unique_cache.get(&key) else {
519 return Err(SdkError::VerificationError(
520 "directory unique cache entry disappeared".to_string(),
521 ));
522 };
523 for payload in &entry.payloads {
524 let value = strip_cached_unique_payload(field_name, payload)?;
525 visitor(value)?;
526 }
527 Ok(())
528 }
529
530 pub fn query_unique_state(&mut self, field_name: &str) -> Result<()> {
531 self.clear_unique_state();
532 let key = self.ensure_unique_cache(field_name)?;
533 self.unique_state = Some(DirectoryUniqueState { key, index: 0 });
534 Ok(())
535 }
536
537 pub fn restart_unique_state(&mut self) {
538 if let Some(state) = &mut self.unique_state {
539 state.index = 0;
540 }
541 }
542
543 pub fn clear_unique_state(&mut self) {
544 self.unique_state = None;
545 for reader in &mut self.files {
546 reader.clear_unique_state();
547 }
548 }
549
550 pub fn enumerate_unique_payload(&mut self) -> Result<Option<Vec<u8>>> {
551 let Some(state) = &mut self.unique_state else {
552 return Ok(None);
553 };
554 let Some(entry) = self.unique_cache.get(&state.key) else {
555 return Err(SdkError::VerificationError(
556 "directory unique state references a missing cache entry".to_string(),
557 ));
558 };
559 if state.index >= entry.payloads.len() {
560 return Ok(None);
561 }
562 let payload = entry.payloads[state.index].clone();
563 state.index += 1;
564 Ok(Some(payload))
565 }
566
567 fn ensure_unique_cache(&mut self, field_name: &str) -> Result<DirectoryUniqueCacheKey> {
568 let key = self.unique_cache_key(field_name);
569 if self.unique_cache.contains_key(&key) {
570 self.touch_unique_cache_key(&key);
571 return Ok(key);
572 }
573
574 let mut payloads = self.build_unique_cache_payloads(field_name)?;
575 let refreshed_key = self.unique_cache_key(field_name);
576 if refreshed_key != key {
577 payloads = self.build_unique_cache_payloads(field_name)?;
578 }
579 let final_key = if refreshed_key == key {
580 key
581 } else {
582 refreshed_key
583 };
584 self.unique_cache
585 .insert(final_key.clone(), DirectoryUniqueCacheEntry { payloads });
586 self.touch_unique_cache_key(&final_key);
587 self.enforce_unique_cache_capacity();
588 #[cfg(test)]
589 {
590 self.unique_cache_builds += 1;
591 }
592 Ok(final_key)
593 }
594
595 fn touch_unique_cache_key(&mut self, key: &DirectoryUniqueCacheKey) {
596 if let Some(index) = self
597 .unique_cache_order
598 .iter()
599 .position(|existing| existing == key)
600 {
601 self.unique_cache_order.remove(index);
602 }
603 self.unique_cache_order.push_back(key.clone());
604 }
605
606 fn enforce_unique_cache_capacity(&mut self) {
607 let active_key = self.unique_state.as_ref().map(|state| state.key.clone());
608 let mut skipped_active = false;
609 while self.unique_cache_order.len() > DIRECTORY_UNIQUE_CACHE_CAPACITY {
610 let Some(evicted) = self.unique_cache_order.pop_front() else {
611 break;
612 };
613 if active_key.as_ref() == Some(&evicted) {
614 if skipped_active {
615 self.unique_cache_order.push_front(evicted);
616 break;
617 }
618 self.unique_cache_order.push_back(evicted);
619 skipped_active = true;
620 continue;
621 }
622 self.unique_cache.remove(&evicted);
623 }
624 }
625
626 fn build_unique_cache_payloads(&mut self, field_name: &str) -> Result<Vec<Vec<u8>>> {
627 let mut seen = HashSet::new();
628 let mut payloads = Vec::new();
629 for reader in &mut self.files {
630 reader.visit_unique_values(field_name, |value| {
631 if seen.insert(value.to_vec()) {
632 payloads.push(cached_unique_payload(field_name, value));
633 }
634 Ok(())
635 })?;
636 }
637 Ok(payloads)
638 }
639
640 fn unique_cache_key(&self, field_name: &str) -> DirectoryUniqueCacheKey {
641 DirectoryUniqueCacheKey {
642 field_name: field_name.to_string(),
643 files: self
644 .files
645 .iter()
646 .map(|reader| {
647 let header = reader.header();
648 DirectoryUniqueFileSignature {
649 file_id: header.file_id,
650 n_objects: header.n_objects,
651 n_entries: header.n_entries,
652 n_data: header.n_data,
653 n_fields: header.n_fields,
654 head_entry_seqnum: header.head_entry_seqnum,
655 tail_entry_seqnum: header.tail_entry_seqnum,
656 head_entry_realtime: header.head_entry_realtime,
657 tail_entry_realtime: header.tail_entry_realtime,
658 tail_entry_monotonic: header.tail_entry_monotonic,
659 tail_entry_boot_id: header.tail_entry_boot_id,
660 }
661 })
662 .collect(),
663 }
664 }
665
666 #[cfg(test)]
667 pub(crate) fn unique_cache_builds_for_tests(&self) -> usize {
668 self.unique_cache_builds
669 }
670
671 pub fn list_boots(&self) -> Vec<BootInfo> {
672 let mut boots: HashMap<String, (i64, i64)> = HashMap::new();
673 for reader in &self.files {
674 let header = reader.cached_header().header;
675 let boot_id = hex::encode(header.tail_entry_boot_id);
676 let first = header.head_entry_realtime as i64;
677 let last = header.tail_entry_realtime as i64;
678 boots
679 .entry(boot_id)
680 .and_modify(|range| {
681 range.0 = range.0.min(first);
682 range.1 = range.1.max(last);
683 })
684 .or_insert((first, last));
685 }
686
687 let mut out: Vec<_> = boots
688 .into_iter()
689 .map(|(boot_id, (first_entry, last_entry))| BootInfo {
690 index: 0,
691 boot_id,
692 first_entry,
693 last_entry,
694 })
695 .collect();
696 out.sort_by_key(|boot| boot.first_entry);
697 let base = 1 - out.len() as i64;
698 for (idx, boot) in out.iter_mut().enumerate() {
699 boot.index = base + idx as i64;
700 }
701 out
702 }
703
704 pub fn add_match(&mut self, data: &[u8]) {
705 for reader in &mut self.files {
706 reader.add_match(data);
707 }
708 self.reset_merge_state();
709 }
710
711 pub fn add_conjunction(&mut self) -> Result<()> {
712 for reader in &mut self.files {
713 reader.add_conjunction()?;
714 }
715 self.reset_merge_state();
716 Ok(())
717 }
718
719 pub fn add_disjunction(&mut self) -> Result<()> {
720 for reader in &mut self.files {
721 reader.add_disjunction()?;
722 }
723 self.reset_merge_state();
724 Ok(())
725 }
726
727 pub fn flush_matches(&mut self) {
728 for reader in &mut self.files {
729 reader.flush_matches();
730 }
731 self.reset_merge_state();
732 }
733
734 fn reset_merge_state(&mut self) {
735 self.index = usize::MAX;
736 self.current_key = None;
737 self.direction = None;
738 self.realtime_seek_bound = None;
739 self.reset_candidates();
740 }
741
742 fn can_step_sequential(&self, direction: Direction) -> bool {
743 if !self.non_overlapping || self.pending_realtime_seek.is_some() {
744 return false;
745 }
746 if self.direction.is_some_and(|current| current != direction) && self.current_key.is_some()
747 {
748 return false;
749 }
750 true
751 }
752
753 fn step_sequential(&mut self, direction: Direction) -> Result<bool> {
754 if self.files.is_empty() {
755 self.clear_current_directory_entry();
756 return Ok(false);
757 }
758
759 if self.direction != Some(direction) {
760 self.reset_sequential_direction(direction);
761 }
762
763 match direction {
764 Direction::Forward => self.step_sequential_forward(),
765 Direction::Backward => self.step_sequential_backward(),
766 }
767 }
768
769 fn reset_sequential_direction(&mut self, direction: Direction) {
770 match direction {
771 Direction::Forward => {
772 for reader in &mut self.files {
773 reader.seek_head();
774 }
775 self.index = 0;
776 }
777 Direction::Backward => {
778 for reader in &mut self.files {
779 reader.seek_tail();
780 }
781 self.index = self.files.len() - 1;
782 }
783 }
784 self.reset_candidates();
785 self.current_key = None;
786 self.realtime_seek_bound = None;
787 self.direction = Some(direction);
788 }
789
790 fn step_sequential_forward(&mut self) -> Result<bool> {
791 if self.index == usize::MAX {
792 self.index = 0;
793 }
794 while self.index < self.files.len() {
795 if self.files[self.index].next()? {
796 self.current_key = Some(self.files[self.index].current_directory_entry_key()?);
797 return Ok(true);
798 }
799 self.index += 1;
800 }
801 self.finish_sequential_end()
802 }
803
804 fn step_sequential_backward(&mut self) -> Result<bool> {
805 if self.index >= self.files.len() {
806 self.index = self.files.len() - 1;
807 }
808 loop {
809 if self.files[self.index].previous()? {
810 self.current_key = Some(self.files[self.index].current_directory_entry_key()?);
811 return Ok(true);
812 }
813 if self.index == 0 {
814 break;
815 }
816 self.index -= 1;
817 }
818 self.finish_sequential_end()
819 }
820
821 fn finish_sequential_end(&mut self) -> Result<bool> {
822 self.clear_current_directory_entry();
823 Ok(false)
824 }
825
826 fn clear_current_directory_entry(&mut self) {
827 self.index = usize::MAX;
828 self.current_key = None;
829 }
830}
831
832pub(super) fn is_journal_file_name(path: &Path) -> bool {
833 path.file_name()
834 .and_then(|name| name.to_str())
835 .is_some_and(|name| {
836 name.ends_with(".journal")
837 || name.ends_with(".journal~")
838 || name.ends_with(".journal.zst")
839 || name.ends_with(".journal~.zst")
840 })
841}
842
843fn collect_journal_files(path: &Path) -> Result<Vec<PathBuf>> {
844 let entries: Vec<_> = std::fs::read_dir(path)?.collect::<std::io::Result<Vec<_>>>()?;
845 let mut files = Vec::new();
846
847 for entry in &entries {
848 let file_path = entry.path();
849 if file_path.is_file() && is_journal_file_name(&file_path) {
850 files.push(file_path);
851 }
852 }
853
854 for entry in &entries {
855 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
856 continue;
857 };
858 if !is_journal_subdir_name(&name) {
859 continue;
860 }
861 let child_path = entry.path();
862 if !child_path.is_dir() {
863 continue;
864 }
865 let Ok(children) = std::fs::read_dir(&child_path) else {
866 continue;
867 };
868 for child in children.flatten() {
869 let file_path = child.path();
870 if file_path.is_file() && is_journal_file_name(&file_path) {
871 files.push(file_path);
872 }
873 }
874 }
875
876 files.sort();
877 Ok(files)
878}
879
880fn cached_unique_payload(field_name: &str, value: &[u8]) -> Vec<u8> {
881 let mut payload = Vec::with_capacity(field_name.len() + 1 + value.len());
882 payload.extend_from_slice(field_name.as_bytes());
883 payload.push(b'=');
884 payload.extend_from_slice(value);
885 payload
886}
887
888fn strip_cached_unique_payload<'a>(field_name: &str, payload: &'a [u8]) -> Result<&'a [u8]> {
889 payload
890 .strip_prefix(field_name.as_bytes())
891 .and_then(|rest| rest.strip_prefix(b"="))
892 .ok_or_else(|| {
893 SdkError::VerificationError(
894 "directory unique cache payload does not match requested field".to_string(),
895 )
896 })
897}
898
899fn is_journal_subdir_name(name: &str) -> bool {
900 if name.contains('.') {
901 return false;
902 }
903 id128_string_valid(name)
904}
905
906fn id128_string_valid(s: &str) -> bool {
907 match s.len() {
908 32 => s.bytes().all(|byte| byte.is_ascii_hexdigit()),
909 36 => s.bytes().enumerate().all(|(idx, byte)| {
910 if matches!(idx, 8 | 13 | 18 | 23) {
911 byte == b'-'
912 } else {
913 byte.is_ascii_hexdigit()
914 }
915 }),
916 _ => false,
917 }
918}
919
920fn build_directory_boot_newest(files: &[FileReader]) -> HashMap<[u8; 16], DirectoryBootNewest> {
921 let mut newest: HashMap<[u8; 16], DirectoryBootNewest> = HashMap::new();
922 for reader in files {
923 let header = reader.cached_header();
924 if header.header.tail_entry_boot_id == [0; 16] {
925 continue;
926 }
927 let replace = match newest.get(&header.header.tail_entry_boot_id) {
928 None => true,
929 Some(current) => header.header.tail_entry_monotonic > current.monotonic,
930 };
931 if replace {
932 newest.insert(
933 header.header.tail_entry_boot_id,
934 DirectoryBootNewest {
935 machine_id: header.header.machine_id,
936 monotonic: header.header.tail_entry_monotonic,
937 realtime: header.header.tail_entry_realtime,
938 },
939 );
940 }
941 }
942 newest
943}
944
945fn directory_files_non_overlapping(files: &[FileReader]) -> bool {
946 if files.is_empty() {
947 return false;
948 }
949
950 for pair in files.windows(2) {
951 let previous = pair[0].cached_header().header;
952 let next = pair[1].cached_header().header;
953 if previous.seqnum_id != next.seqnum_id
954 || previous.tail_entry_seqnum == 0
955 || next.head_entry_seqnum == 0
956 || previous.tail_entry_seqnum >= next.head_entry_seqnum
957 || previous.tail_entry_realtime == 0
958 || next.head_entry_realtime == 0
959 || previous.tail_entry_realtime >= next.head_entry_realtime
960 {
961 return false;
962 }
963 }
964
965 true
966}
967
968fn cmp_u64(a: u64, b: u64) -> i8 {
969 match a.cmp(&b) {
970 std::cmp::Ordering::Less => -1,
971 std::cmp::Ordering::Equal => 0,
972 std::cmp::Ordering::Greater => 1,
973 }
974}