diffs_rs/reading/
literal.rs1use std::{
2 io::{self, Read},
3 ops::Range,
4};
5
6use anyhow::{Context, Result, ensure};
7use memchr::memmem::{Finder, FinderRev};
8
9pub(crate) const QUERY_BYTES: usize = 4096;
10const CHUNK: usize = 64 * 1024;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Direction {
14 Forward,
15 Backward,
16}
17
18pub(crate) struct Literal<'a> {
19 forward: Finder<'a>,
20 backward: FinderRev<'a>,
21 buffer: Vec<u8>,
22}
23
24pub(crate) fn validate(query: &str) -> Result<()> {
25 ensure!(!query.is_empty(), "Search text is empty");
26 ensure!(
27 query.len() <= QUERY_BYTES,
28 "Search text exceeds {QUERY_BYTES} UTF-8 bytes"
29 );
30 ensure!(
31 !query.contains(['\n', '\r']),
32 "Search text must be one line"
33 );
34 Ok(())
35}
36
37impl<'a> Literal<'a> {
38 pub(crate) fn new(query: &'a str) -> Result<Self> {
39 validate(query)?;
40 Ok(Self {
41 forward: Finder::new(query.as_bytes()),
42 backward: FinderRev::new(query.as_bytes()),
43 buffer: Vec::with_capacity(CHUNK + query.len() - 1),
44 })
45 }
46
47 pub(crate) fn find(
51 &mut self,
52 input: &mut impl Read,
53 starts: Range<u64>,
54 direction: Direction,
55 check: &impl Fn() -> Result<()>,
56 ) -> Result<Option<Range<u64>>> {
57 ensure!(starts.start <= starts.end, "Invalid search byte range");
58 check()?;
59 self.buffer.clear();
60 if starts.is_empty() {
61 return Ok(None);
62 }
63 let needle = self.forward.needle().len();
64 let mut base = starts.start;
65 let mut found = None;
66 loop {
67 check()?;
68 let retained = self.buffer.len();
69 self.buffer.resize(retained + CHUNK, 0);
70 let count = loop {
71 match input.read(&mut self.buffer[retained..]) {
72 Err(error) if error.kind() == io::ErrorKind::Interrupted => check()?,
73 result => break result?,
74 }
75 };
76 self.buffer.truncate(retained + count);
77 check()?;
78 if count == 0 {
79 return Ok(found);
80 }
81 if self.buffer.len() >= needle {
82 let complete = base
83 .checked_add((self.buffer.len() - needle + 1) as u64)
84 .context("Search byte offset overflow")?;
85 let eligible = complete.min(starts.end);
86 if eligible > base {
87 let end = usize::try_from(eligible - base)? + needle - 1;
88 let haystack = &self.buffer[..end];
89 let local = match direction {
90 Direction::Forward => self.forward.find(haystack),
91 Direction::Backward => self.backward.rfind(haystack),
92 };
93 if let Some(local) = local {
94 let first = base
95 .checked_add(local as u64)
96 .context("Search byte offset overflow")?;
97 let last = first
98 .checked_add(needle as u64)
99 .context("Search byte offset overflow")?;
100 found = Some(first..last);
101 }
102 }
103 if (found.is_some() && direction == Direction::Forward) || complete >= starts.end {
104 check()?;
105 return Ok(found);
106 }
107 }
108 let keep = self.buffer.len().min(needle - 1);
109 let consumed = self.buffer.len() - keep;
110 self.buffer.copy_within(consumed.., 0);
111 self.buffer.truncate(keep);
112 base = base
113 .checked_add(consumed as u64)
114 .context("Search byte offset overflow")?;
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests;