feat(04-04): add StdinSource, URLSource, and ClipboardSource
- StdinSource reads from an injectable io.Reader (INPUT-03) - URLSource fetches http/https with 30s timeout, 50MB cap, scheme whitelist, and Content-Type filter (INPUT-04) - ClipboardSource wraps atotto/clipboard with graceful fallback for missing tooling (INPUT-05) - emitByteChunks local helper mirrors file.go windowing to stay independent of sibling wave-1 plans - Tests cover happy path, cancellation, redirects, oversize bodies, binary content types, scheme rejection, and clipboard error paths
This commit is contained in:
85
pkg/engine/sources/stdin.go
Normal file
85
pkg/engine/sources/stdin.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package sources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/salvacybersec/keyhunter/pkg/types"
|
||||
)
|
||||
|
||||
// StdinSource reads content from an io.Reader (defaults to os.Stdin) and
|
||||
// emits overlapping chunks. Used when a user runs `keyhunter scan stdin`
|
||||
// or `keyhunter scan -`.
|
||||
type StdinSource struct {
|
||||
Reader io.Reader
|
||||
ChunkSize int
|
||||
}
|
||||
|
||||
// NewStdinSource returns a StdinSource bound to os.Stdin.
|
||||
func NewStdinSource() *StdinSource {
|
||||
return &StdinSource{Reader: os.Stdin, ChunkSize: defaultChunkSize}
|
||||
}
|
||||
|
||||
// NewStdinSourceFrom returns a StdinSource bound to the given reader
|
||||
// (used primarily by tests).
|
||||
func NewStdinSourceFrom(r io.Reader) *StdinSource {
|
||||
return &StdinSource{Reader: r, ChunkSize: defaultChunkSize}
|
||||
}
|
||||
|
||||
// Chunks reads the entire input, then emits it as overlapping chunks.
|
||||
func (s *StdinSource) Chunks(ctx context.Context, out chan<- types.Chunk) error {
|
||||
if s.Reader == nil {
|
||||
s.Reader = os.Stdin
|
||||
}
|
||||
data, err := io.ReadAll(s.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return emitByteChunks(ctx, data, "stdin", s.ChunkSize, out)
|
||||
}
|
||||
|
||||
// emitByteChunks emits overlapping chunks from an in-memory byte slice.
|
||||
// Local helper for stdin/url/clipboard sources; mirrors the chunk-windowing
|
||||
// logic in file.go so this plan does not depend on sibling plans' helpers.
|
||||
func emitByteChunks(ctx context.Context, data []byte, source string, size int, out chan<- types.Chunk) error {
|
||||
if size <= 0 {
|
||||
size = defaultChunkSize
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(data) <= size {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case out <- types.Chunk{Data: data, Source: source, Offset: 0}:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var offset int64
|
||||
for start := 0; start < len(data); start += size - chunkOverlap {
|
||||
end := start + size
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
chunk := types.Chunk{
|
||||
Data: data[start:end],
|
||||
Source: source,
|
||||
Offset: offset,
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case out <- chunk:
|
||||
}
|
||||
offset += int64(end - start)
|
||||
if end == len(data) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user