Skip to main content

keyhog_sources/
stdin.rs

1//! Standard input source: reads piped input as a single chunk for scanning.
2
3use keyhog_core::{Chunk, ChunkMetadata, Source, SourceError};
4use std::io::Read;
5
6// Security boundary: stdin is intentionally capped so piped input cannot force
7// an unbounded allocation during scan startup.
8const MAX_STDIN_BYTES: usize = 10 * 1024 * 1024;
9
10/// Reads all of stdin as a single chunk.
11///
12/// # Examples
13///
14/// ```rust
15/// use keyhog_sources::StdinSource;
16/// use keyhog_core::Source;
17///
18/// let source = StdinSource;
19/// assert_eq!(source.name(), "stdin");
20/// ```
21pub struct StdinSource;
22
23impl Source for StdinSource {
24    fn name(&self) -> &str {
25        "stdin"
26    }
27
28    fn chunks(&self) -> Box<dyn Iterator<Item = Result<Chunk, SourceError>> + '_> {
29        let stdin_read = read_stdin_limited(MAX_STDIN_BYTES);
30
31        Box::new(std::iter::once(match stdin_read {
32            Ok(data) => Ok(Chunk {
33                data: data.into(),
34                metadata: ChunkMetadata {
35                    base_offset: 0,
36                    source_type: "stdin".into(),
37                    path: None,
38                    commit: None,
39                    author: None,
40                    date: None,
41                    mtime_ns: None,
42                    size_bytes: None,
43                },
44            }),
45            Err(e) => Err(SourceError::Io(e)),
46        }))
47    }
48
49    fn as_any(&self) -> &dyn std::any::Any {
50        self
51    }
52}
53
54fn read_stdin_limited(max_bytes: usize) -> std::io::Result<String> {
55    read_to_string_limited(&mut std::io::stdin().lock(), max_bytes)
56}
57
58fn read_to_string_limited(reader: &mut impl Read, max_bytes: usize) -> std::io::Result<String> {
59    let mut bytes = Vec::new();
60    // Read at most `max_bytes + 1` so oversized stdin is rejected before we
61    // hand a giant buffer to the scanner.
62    reader.take(max_bytes as u64 + 1).read_to_end(&mut bytes)?;
63
64    if bytes.len() > max_bytes {
65        return Err(std::io::Error::other(format!(
66            "stdin exceeds {} byte limit",
67            max_bytes
68        )));
69    }
70
71    String::from_utf8(bytes).map_err(|error| {
72        std::io::Error::new(
73            std::io::ErrorKind::InvalidData,
74            format!(
75                "stdin is not valid UTF-8 near byte {}: {error}",
76                error.utf8_error().valid_up_to()
77            ),
78        )
79    })
80}