Skip to main content

keyhog_core/
source.rs

1//! Source trait and chunk types: the abstraction for pluggable input backends.
2
3use serde::Serialize;
4use crate::SensitiveString;
5use thiserror::Error;
6
7/// A scannable chunk of text with metadata about where it came from.
8///
9/// # Examples
10///
11/// ```rust
12/// use keyhog_core::{Chunk, ChunkMetadata};
13///
14/// let chunk = Chunk {
15///     data: "API_KEY=sk_live_example".into(),
16///     metadata: ChunkMetadata {
17                    
18///         source_type: "filesystem".into(),
19///         path: Some("app.env".into()),
20///         base_offset: 0,
21///         commit: None,
22///         author: None,
23///         date: None,
24///     },
25/// };
26///
27/// assert_eq!(chunk.metadata.path.as_deref(), Some("app.env"));
28/// ```
29#[derive(Debug, Clone, Serialize)]
30pub struct Chunk {
31    /// UTF-8 text content to scan.
32    pub data: SensitiveString,
33    /// Provenance details used in findings and reporters.
34    pub metadata: ChunkMetadata,
35}
36
37/// Metadata that tracks the source location for a scanned chunk.
38///
39/// # Examples
40///
41/// ```rust
42/// use keyhog_core::ChunkMetadata;
43///
44/// let metadata = ChunkMetadata {
45                    
46///     source_type: "git-diff".into(),
47///     path: Some("src/lib.rs".into()),
48///     base_offset: 0,
49///     commit: Some("abc123".into()),
50///     author: Some("Dev".into()),
51///     date: Some("2026-03-26T00:00:00Z".into()),
52/// };
53///
54/// assert_eq!(metadata.source_type, "git-diff");
55/// ```
56#[derive(Debug, Clone, Serialize, Default)]
57pub struct ChunkMetadata {
58                    
59    pub source_type: String,
60    pub path: Option<String>,
61    pub commit: Option<String>,
62    pub author: Option<String>,
63    pub date: Option<String>,
64    pub base_offset: usize,
65}
66
67/// Produces chunks of text for the scanner to process.
68/// Each implementation handles a different input source.
69///
70/// # Examples
71///
72/// ```rust
73/// use keyhog_core::{Chunk, ChunkMetadata, Source, SourceError};
74///
75/// struct StaticSource;
76///
77/// impl Source for StaticSource {
78///     fn name(&self) -> &str {
79///         "static"
80///     }
81///
82///     fn chunks(&self) -> Box<dyn Iterator<Item = Result<Chunk, SourceError>> + '_> {
83///         Box::new(std::iter::once(Ok(Chunk {
84///             data: "TOKEN=value".into(),
85///             metadata: ChunkMetadata {
86                    
87///                 source_type: "static".into(),
88///                 path: None,
89///                 base_offset: 0,
90///                 commit: None,
91///                 author: None,
92///                 date: None,
93///             },
94///         })))
95///     }
96///
97///     fn as_any(&self) -> &dyn std::any::Any {
98///         self
99///     }
100/// }
101///
102/// let source = StaticSource;
103/// assert_eq!(source.name(), "static");
104/// ```
105pub trait Source: Send + Sync {
106    /// Human-readable source name used in warnings and telemetry.
107    fn name(&self) -> &str;
108    /// Yield all readable chunks from this source.
109    fn chunks(&self) -> Box<dyn Iterator<Item = Result<Chunk, SourceError>> + '_>;
110    /// Support downcasting to concrete types.
111    fn as_any(&self) -> &dyn std::any::Any;
112}
113
114/// Errors returned by input sources while enumerating or reading content.
115///
116/// # Examples
117///
118/// ```rust
119/// use keyhog_core::SourceError;
120///
121/// let error = SourceError::Other("pass a readable file or directory".into());
122/// assert!(error.to_string().contains("Fix"));
123/// ```
124#[derive(Debug, Error)]
125pub enum SourceError {
126    #[error(
127        "failed to read source: {0}. Fix: check the path exists, is readable, and is not a broken symlink"
128    )]
129    Io(#[from] std::io::Error),
130    #[error(
131        "failed to access git source: {0}. Fix: run inside a valid git repository and verify the requested refs exist"
132    )]
133    Git(String),
134    #[error(
135        "failed to read source: {0}. Fix: adjust the source settings or input so KeyHog can read plain text safely"
136    )]
137    Other(String),
138}