Skip to main content

tale_ndjson/readers/
stdin.rs

1//! Handle stdin with offsets. We never multiplex stdin, so this is a simpler
2//! case than many others in some ways, and a more challenging one in others.
3//! Specifically, scrolling *back* by negative offsets can be tricky when the
4//! offsets are large.
5
6use std::io::{self, BufRead, BufReader, Read, Write};
7use std::time::{Duration, Instant};
8
9use bytes::BytesMut;
10use miette::{IntoDiagnostic, Result, WrapErr};
11
12use crate::defaults::io::*;
13use crate::defaults::memory::*;
14use crate::defaults::processing::BLOCK_SIZE;
15use crate::{config, process_line, strip_line_ending};
16
17/// Entry point for processing stdin all the ways we need to handle it.
18pub fn handle_stdin() -> Result<()> {
19    let offset = config::offset();
20    let offset_unit = config::offset_unit();
21
22    let mut processor = StdinProcessor::new();
23
24    if offset == 0 {
25        return processor.tail();
26    }
27
28    match (offset.is_positive(), offset_unit) {
29        // Positive offsets: skip first N units
30        (true, config::OffsetUnit::Lines) => processor.skip_lines(offset as u64),
31        (true, config::OffsetUnit::Bytes) => processor.skip_bytes(offset as u64),
32        (true, config::OffsetUnit::Blocks) => {
33            let bytes_to_skip = (offset as u64) * BLOCK_SIZE;
34            processor.skip_bytes(bytes_to_skip)
35        }
36
37        // Negative offsets: show last N units
38        (false, config::OffsetUnit::Lines) => processor.backtrack_lines((-offset) as u64),
39        (false, config::OffsetUnit::Bytes) => processor.backtrack_bytes((-offset) as u64),
40        (false, config::OffsetUnit::Blocks) => processor.backtrack_bytes(((-offset) as u64) * BLOCK_SIZE),
41    }
42}
43
44/// Handles common stdin processing patterns with automatic flushing and tailing
45/// support
46pub struct StdinProcessor<'a> {
47    inlock: io::StdinLock<'a>,
48    outlock: io::StdoutLock<'a>,
49    buffer: BytesMut,
50    line: String,
51    count: u16,
52}
53
54impl<'a> Default for StdinProcessor<'a> {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl<'a> StdinProcessor<'a> {
61    /// Create a new StdinProcessor with standard buffer sizes
62    pub fn new() -> Self {
63        Self {
64            inlock: io::stdin().lock(),
65            outlock: io::stdout().lock(),
66            buffer: BytesMut::with_capacity(OUTPUT_BUFFER_CAPACITY),
67            line: String::with_capacity(LINE_CAPACITY),
68            count: 0,
69        }
70    }
71
72    /// Process a single line through the formatting pipeline
73    pub fn process_line(&mut self, line: &str) -> Result<()> {
74        process_line(line, &mut self.buffer, &mut self.outlock).with_context(|| "Failed to process line")?;
75        self.count += 1;
76        self.flush_if_needed()
77    }
78
79    /// Flush output if we've processed enough lines
80    pub fn flush_if_needed(&mut self) -> Result<()> {
81        if self.count >= FLUSH_LINE_COUNT {
82            self.outlock.flush().into_diagnostic()?;
83            self.count = 0;
84        }
85        Ok(())
86    }
87
88    /// Force flush output
89    pub fn flush(&mut self) -> Result<()> {
90        self.outlock.flush().into_diagnostic()?;
91        self.count = 0;
92        Ok(())
93    }
94
95    /// Read a line from stdin, stripping line endings
96    pub fn read_line(&mut self) -> Result<usize> {
97        self.line.clear();
98        let bytes_read = self.inlock.read_line(&mut self.line).into_diagnostic()?;
99        if bytes_read > 0 {
100            strip_line_ending(&mut self.line);
101        }
102        Ok(bytes_read)
103    }
104
105    /// Get the current line content
106    pub fn line(&self) -> &str {
107        &self.line
108    }
109
110    /// Process all remaining input until EOF
111    pub fn process_to_end(&mut self) -> Result<()> {
112        while self.read_line()? != 0 {
113            let line = self.line().to_string();
114            self.process_line(&line)?;
115        }
116        self.flush()
117    }
118
119    /// We have a partial buffer left over from a read. Seek back,
120    /// then continue processing.
121    pub fn handle_overshoot(&mut self, overshoot: &[u8]) -> Result<()> {
122        // Process any complete lines in the overshoot buffer using byte operations
123        let mut start = 0;
124        for (i, &byte) in overshoot.iter().enumerate() {
125            if byte == b'\n' {
126                // Found a complete line
127                let line_bytes = &overshoot[start..i];
128                let line = String::from_utf8_lossy(line_bytes);
129                self.process_line(&line)?;
130                start = i + 1;
131            }
132        }
133
134        // If there's a partial line remaining, add it to our line buffer
135        if start < overshoot.len() {
136            let remaining_bytes = &overshoot[start..];
137            let remaining_str = String::from_utf8_lossy(remaining_bytes);
138            self.line.push_str(&remaining_str);
139        }
140
141        // Read the rest of the partial line (if any)
142        if !self.line.is_empty() && self.inlock.read_line(&mut self.line).into_diagnostic()? > 0 {
143            strip_line_ending(&mut self.line);
144            let line = self.line().to_string();
145            self.process_line(&line)?;
146        }
147
148        // Now process the rest normally
149        self.tail()
150    }
151
152    /// Enter normal processing mode - process input until EOF, then poll for
153    /// more
154    pub fn tail(&mut self) -> Result<()> {
155        self.process_to_end()?;
156        if !config::tailing() {
157            return Ok(());
158        }
159
160        let mut last_flush = Instant::now();
161        loop {
162            std::thread::sleep(Duration::from_millis(100));
163
164            match self.read_line()? {
165                0 => continue, // EOF - keep polling
166                _ => {
167                    let line = self.line().to_string();
168                    self.process_line(&line)?;
169                    if last_flush.elapsed() >= TAIL_FLUSH_INTERVAL {
170                        self.flush()?;
171                        last_flush = Instant::now();
172                    }
173                }
174            }
175        }
176    }
177
178    pub fn skip_lines(&mut self, count: u64) -> Result<()> {
179        // Skip the requested number of lines
180        let mut lines_skipped = 0u64;
181        while lines_skipped < count {
182            match self.read_line()? {
183                0 => {
184                    // EOF reached before skipping enough lines - nothing to output
185                    return Ok(());
186                }
187                _ => {
188                    lines_skipped += 1;
189                }
190            }
191        }
192        self.tail()
193    }
194
195    // skip bytes then keep going, tailing if config says to tail
196    pub fn skip_bytes(&mut self, to_skip: u64) -> Result<()> {
197        let mut buffer = [0u8; READ_BUFFER_SIZE];
198        let mut bytes_skipped = 0u64;
199
200        while bytes_skipped < to_skip {
201            let bytes_read = self.inlock.read(&mut buffer).into_diagnostic()?;
202            if bytes_read == 0 {
203                // EOF reached before skipping enough bytes - nothing to output
204                return Ok(());
205            }
206
207            let bytes_to_consume = std::cmp::min(bytes_read as u64, to_skip - bytes_skipped);
208            bytes_skipped += bytes_to_consume;
209
210            // If we read more than we needed to skip, we need to handle the overshoot
211            if bytes_skipped == to_skip && bytes_to_consume < bytes_read as u64 {
212                let overshoot_start = bytes_to_consume as usize;
213                let overshoot = &buffer[overshoot_start..bytes_read];
214                return self.handle_overshoot(overshoot);
215            }
216        }
217
218        // Process remaining input normally (no overshoot)
219        self.tail()
220    }
221
222    pub fn backtrack_bytes(&mut self, bytes_to_show: u64) -> Result<()> {
223        let mut circular_buffer = CircularByteBuffer::new(bytes_to_show as usize);
224
225        // Read all input into circular buffer
226        loop {
227            let bytes_read = self.inlock.read(&mut self.buffer).into_diagnostic()?;
228            if bytes_read == 0 {
229                break; // EOF
230            }
231            circular_buffer.write(&self.buffer[..bytes_read]);
232        }
233
234        // Check if we have any data
235        if circular_buffer.is_empty() {
236            return Ok(()); // No input
237        }
238
239        let mut overshoot: Vec<u8> = Vec::new();
240        let output_bytes = circular_buffer.extract_last_bytes();
241        let process_this = match find_last_char(output_bytes.as_slice(), b'\n') {
242            Some(last_line_ending) => {
243                overshoot = output_bytes[last_line_ending + 1..].to_vec();
244                output_bytes[..last_line_ending].to_vec()
245            }
246            None => output_bytes[..].to_vec(),
247        };
248
249        // Process the output bytes line by line
250        let output_str = String::from_utf8_lossy(&process_this);
251        for line in output_str.lines() {
252            self.process_line(line)?;
253        }
254        // if we have anything left over, we need to handle the overshoot
255        if !overshoot.is_empty() {
256            self.handle_overshoot(overshoot.as_slice())
257        } else {
258            self.tail()
259        }
260    }
261
262    /// Show last N lines from stdin (adaptive approach with circular buffer)
263    pub fn backtrack_lines(&mut self, lines_to_show: u64) -> Result<()> {
264        use std::collections::VecDeque;
265
266        use tempfile::NamedTempFile;
267
268        let mut line_buffer: VecDeque<String> = VecDeque::with_capacity(lines_to_show as usize);
269        let mut memory_used = 0usize;
270        let mut temp_file: Option<NamedTempFile> = None;
271
272        // Read all input, keeping only the last N lines
273        loop {
274            let bytes_read = self.read_line()?;
275            if bytes_read == 0 {
276                break; // EOF
277            }
278
279            // Check if we need to switch to temp file mode
280            if memory_used > MEMORY_LIMIT_BYTES && temp_file.is_none() {
281                // Create temp file and write current buffer to it
282                let mut temp = NamedTempFile::new()
283                    .into_diagnostic()
284                    .wrap_err("Failed to create temporary file for large stdin backtrack")?;
285
286                // Write existing buffer to temp file
287                for line in &line_buffer {
288                    writeln!(temp, "{}", line)
289                        .into_diagnostic()
290                        .wrap_err("Failed to write to temporary file")?;
291                }
292
293                temp_file = Some(temp);
294
295                // Clear memory buffer since we're now using temp file
296                line_buffer.clear();
297                memory_used = 0;
298            }
299
300            match &mut temp_file {
301                Some(temp) => {
302                    // Write to temp file
303                    writeln!(temp, "{}", self.line)
304                        .into_diagnostic()
305                        .wrap_err("Failed to write to temporary file")?;
306                }
307                None => {
308                    // Add to circular buffer in memory
309                    if line_buffer.len() >= lines_to_show as usize {
310                        // Remove oldest line and update memory usage
311                        if let Some(old_line) = line_buffer.pop_front() {
312                            memory_used -= old_line.len();
313                        }
314                    }
315                    memory_used += self.line.len();
316                    line_buffer.push_back(self.line.clone());
317                }
318            }
319        }
320
321        // Output the result
322        match temp_file {
323            Some(mut temp) => {
324                // Flush and read back last N lines from temp file
325                temp.flush()
326                    .into_diagnostic()
327                    .wrap_err("Failed to flush temporary file")?;
328                self.read_last_n_lines_from_temp_file(temp, lines_to_show)?;
329            }
330            None => {
331                // Output the buffered lines from memory
332                for buffered_line in line_buffer {
333                    self.process_line(&buffered_line)?;
334                }
335            }
336        }
337
338        self.flush()?;
339        Ok(())
340    }
341
342    /// Read the last N lines from a temporary file
343    fn read_last_n_lines_from_temp_file(
344        &mut self,
345        temp_file: tempfile::NamedTempFile,
346        lines_to_show: u64,
347    ) -> Result<()> {
348        use std::collections::VecDeque;
349        use std::fs::File;
350
351        // Reopen the temp file for reading
352        let file = File::open(temp_file.path())
353            .into_diagnostic()
354            .wrap_err("Failed to open temporary file for reading")?;
355        let reader = BufReader::new(file);
356
357        let mut line_buffer: VecDeque<String> = VecDeque::with_capacity(lines_to_show as usize);
358
359        // Read all lines, keeping only the last N
360        for line_result in reader.lines() {
361            let line = line_result
362                .into_diagnostic()
363                .wrap_err("Failed to read line from temporary file")?;
364
365            if line_buffer.len() >= lines_to_show as usize {
366                line_buffer.pop_front();
367            }
368            line_buffer.push_back(line);
369        }
370
371        // Output the last N lines
372        for line in line_buffer {
373            self.process_line(&line)?;
374        }
375
376        // Temp file will be automatically deleted when NamedTempFile is dropped
377        Ok(())
378    }
379}
380
381/// Circular buffer for efficiently storing and retrieving the last N bytes
382pub struct CircularByteBuffer {
383    buffer: Vec<u8>,
384    pos: usize,
385    total_read: u64,
386    capacity: usize,
387}
388
389impl CircularByteBuffer {
390    /// Create a new circular buffer with the specified capacity
391    pub fn new(capacity: usize) -> Self {
392        Self {
393            buffer: vec![0u8; capacity],
394            pos: 0,
395            total_read: 0,
396            capacity,
397        }
398    }
399
400    /// Write data to the circular buffer
401    pub fn write(&mut self, data: &[u8]) {
402        for &byte in data {
403            self.buffer[self.pos % self.capacity] = byte;
404            self.pos += 1;
405            self.total_read += 1;
406        }
407    }
408
409    /// Extract the last N bytes from the buffer (up to capacity)
410    pub fn extract_last_bytes(&self) -> Vec<u8> {
411        if self.total_read == 0 {
412            return Vec::new();
413        }
414
415        let bytes_to_output = std::cmp::min(self.total_read, self.capacity as u64) as usize;
416
417        if self.total_read >= self.capacity as u64 {
418            // Full circular buffer case - need to wrap around
419            let start_pos = self.pos % self.capacity;
420            let mut result = Vec::with_capacity(bytes_to_output);
421            for i in 0..bytes_to_output {
422                result.push(self.buffer[(start_pos + i) % self.capacity]);
423            }
424            result
425        } else {
426            // Partial buffer case - just take what we have
427            self.buffer[..bytes_to_output].to_vec()
428        }
429    }
430
431    /// Check if the buffer is empty
432    pub fn is_empty(&self) -> bool {
433        self.total_read == 0
434    }
435
436    /// Get the total number of bytes that have been written
437    pub fn total_written(&self) -> u64 {
438        self.total_read
439    }
440}
441
442fn find_last_char(buffer: &[u8], c: u8) -> Option<usize> {
443    buffer.iter().rposition(|&b| b == c)
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn can_find_last_char() {
452        // Test normal case
453        assert_eq!(find_last_char(b"hello\nworld\n", b'\n'), Some(11));
454        assert_eq!(find_last_char(b"hello\nworld", b'\n'), Some(5));
455
456        // Test no match
457        assert_eq!(find_last_char(b"hello world", b'\n'), None);
458
459        // Test empty buffer
460        assert_eq!(find_last_char(b"", b'\n'), None);
461
462        // Test single character
463        assert_eq!(find_last_char(b"\n", b'\n'), Some(0));
464        assert_eq!(find_last_char(b"a", b'\n'), None);
465
466        // Test multiple occurrences
467        assert_eq!(find_last_char(b"\n\n\n", b'\n'), Some(2));
468    }
469
470    #[test]
471    fn circular_buffer_edge_cases() {
472        // Test buffer exactly at capacity
473        let mut buffer = CircularByteBuffer::new(5);
474        buffer.write(b"12345");
475        assert_eq!(buffer.extract_last_bytes(), b"12345");
476
477        // Test buffer overflow
478        buffer.write(b"67890");
479        assert_eq!(buffer.extract_last_bytes(), b"67890");
480
481        // Test partial writes
482        let mut buffer2 = CircularByteBuffer::new(10);
483        buffer2.write(b"abc");
484        assert_eq!(buffer2.extract_last_bytes(), b"abc");
485
486        // Test multiple small writes
487        buffer2.write(b"def");
488        buffer2.write(b"ghi");
489        assert_eq!(buffer2.extract_last_bytes(), b"abcdefghi");
490    }
491
492    #[test]
493    fn backtracking_with_partial_lines() {
494        // Test that backtrack_bytes properly handles partial lines at the end
495        // This tests the logic without actual I/O
496
497        // Case 1: Buffer ends with newline
498        let buffer_with_newline = b"line1\nline2\nline3\n";
499        assert_eq!(find_last_char(buffer_with_newline, b'\n'), Some(17));
500
501        // Case 2: Buffer doesn't end with newline
502        let buffer_without_newline = b"line1\nline2\nline3";
503        assert_eq!(find_last_char(buffer_without_newline, b'\n'), Some(11));
504
505        // Case 3: Buffer with no newlines
506        let buffer_no_newlines = b"single long line without newlines";
507        assert_eq!(find_last_char(buffer_no_newlines, b'\n'), None);
508    }
509}