Skip to main content

diffs_rs/reading/
literal.rs

1use 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    /// The reader begins at `starts.start` within one original source record.
48    /// Bounds restrict match starts, not ends; overlapping matches remain eligible.
49    /// Backward search scans forward and retains only the latest eligible match.
50    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;