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 }