Skip to main content

journal/
directory.rs

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(&current_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}