11//! Wordlist source - generate keys from file of passphrases.
2+ //!
3+ //! Streams the file in chunks to avoid loading entire wordlists into memory.
24
35use anyhow:: Result ;
46use indicatif:: ProgressBar ;
57use rayon:: prelude:: * ;
6- use std:: fs:: File ;
7- use std:: io:: { BufRead , BufReader } ;
8- use std:: path:: Path ;
8+ use std:: fs;
9+ use std:: io:: { BufRead , BufReader , Seek } ;
10+ use std:: path:: { Path , PathBuf } ;
911
1012use super :: { ProcessStats , Source } ;
1113use crate :: derive:: KeyDeriver ;
1214use crate :: matcher:: Matcher ;
1315use crate :: output:: Output ;
1416use crate :: transform:: { Input , Transform } ;
1517
18+ const CHUNK_SIZE : usize = 100_000 ;
19+ const BATCH_SIZE : usize = 1000 ;
20+
1621/// Generate keys from a wordlist file
1722pub struct WordlistSource {
18- lines : Vec < String > ,
23+ path : PathBuf ,
1924}
2025
2126impl WordlistSource {
2227 pub fn from_file ( path : & Path ) -> Result < Self > {
23- let file = File :: open ( path) ?;
24- let reader = BufReader :: new ( file) ;
25- let lines: Vec < String > = reader
26- . lines ( )
27- . filter_map ( |line| match line {
28- Ok ( s) => {
29- let trimmed = s. trim ( ) . to_string ( ) ;
30- if trimmed. is_empty ( ) { None } else { Some ( Ok ( trimmed) ) }
31- }
32- Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: InvalidData => None ,
33- Err ( e) => Some ( Err ( e) ) ,
34- } )
35- . collect :: < std:: io:: Result < Vec < _ > > > ( ) ?;
36-
37- Ok ( Self { lines } )
28+ if !path. exists ( ) {
29+ anyhow:: bail!( "Wordlist file not found: {}" , path. display( ) ) ;
30+ }
31+ if !path. is_file ( ) {
32+ anyhow:: bail!( "Not a file: {}" , path. display( ) ) ;
33+ }
34+ Ok ( Self {
35+ path : path. to_path_buf ( ) ,
36+ } )
3837 }
3938}
4039
@@ -46,51 +45,176 @@ impl Source for WordlistSource {
4645 matcher : Option < & Matcher > ,
4746 output : & dyn Output ,
4847 ) -> Result < ProcessStats > {
49- let pb = ProgressBar :: new ( self . lines . len ( ) as u64 ) ;
48+ let file_size = fs:: metadata ( & self . path ) ?. len ( ) ;
49+ let pb = ProgressBar :: new ( file_size) ;
5050 pb. set_style ( crate :: default_progress_style ( ) ) ;
5151
52-
5352 let stats = std:: sync:: atomic:: AtomicU64 :: new ( 0 ) ;
5453 let matches = std:: sync:: atomic:: AtomicU64 :: new ( 0 ) ;
54+ let mut inputs_processed = 0u64 ;
55+ let mut bytes_consumed = 0u64 ;
56+
57+ let file = std:: fs:: File :: open ( & self . path ) ?;
58+ let mut reader = BufReader :: new ( file) ;
59+ let mut chunk = Vec :: with_capacity ( CHUNK_SIZE ) ;
60+ let mut line_buf = String :: new ( ) ;
61+
62+ loop {
63+ line_buf. clear ( ) ;
64+ let bytes_read = match reader. read_line ( & mut line_buf) {
65+ Ok ( 0 ) => break ,
66+ Ok ( n) => n as u64 ,
67+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: InvalidData => {
68+ // read_line() already consumed the bytes, sync position from reader
69+ bytes_consumed = reader. stream_position ( ) . unwrap_or ( bytes_consumed) ;
70+ pb. set_position ( bytes_consumed) ;
71+ continue ;
72+ }
73+ Err ( e) => return Err ( e. into ( ) ) ,
74+ } ;
5575
56- self . lines . par_chunks ( 1000 ) . for_each ( |chunk| {
57- let inputs: Vec < Input > = chunk
58- . iter ( )
59- . map ( |s| Input :: from_string ( s. clone ( ) ) )
60- . collect ( ) ;
61- let mut buffer = Vec :: with_capacity ( inputs. len ( ) * 2 ) ;
62-
63- for transform in transforms {
64- buffer. clear ( ) ;
65- transform. apply_batch ( & inputs, & mut buffer) ;
66-
67- for ( source, key) in & buffer {
68- let derived = deriver. derive ( key) ;
69-
70- if let Some ( m) = matcher {
71- if let Some ( match_info) = m. check ( & derived) {
72- output
73- . hit ( source, transform. name ( ) , & derived, & match_info)
74- . ok ( ) ;
75- matches. fetch_add ( 1 , std:: sync:: atomic:: Ordering :: Relaxed ) ;
76- }
77- } else {
78- output. key ( source, transform. name ( ) , & derived) . ok ( ) ;
79- }
76+ bytes_consumed += bytes_read;
77+ pb. set_position ( bytes_consumed) ;
8078
81- stats. fetch_add ( 1 , std:: sync:: atomic:: Ordering :: Relaxed ) ;
82- }
79+ let trimmed = line_buf. trim ( ) . to_string ( ) ;
80+ if trimmed. is_empty ( ) {
81+ continue ;
8382 }
8483
85- pb. inc ( chunk. len ( ) as u64 ) ;
86- } ) ;
84+ chunk. push ( trimmed) ;
85+ inputs_processed += 1 ;
86+
87+ if chunk. len ( ) >= CHUNK_SIZE {
88+ process_chunk (
89+ & chunk, transforms, deriver, matcher, output, & stats, & matches,
90+ ) ;
91+ chunk. clear ( ) ;
92+ }
93+ }
94+
95+ if !chunk. is_empty ( ) {
96+ process_chunk (
97+ & chunk, transforms, deriver, matcher, output, & stats, & matches,
98+ ) ;
99+ }
87100
88101 pb. finish_and_clear ( ) ;
89102
90103 Ok ( ProcessStats {
91- inputs_processed : self . lines . len ( ) as u64 ,
104+ inputs_processed,
92105 keys_generated : stats. load ( std:: sync:: atomic:: Ordering :: Relaxed ) ,
93106 matches_found : matches. load ( std:: sync:: atomic:: Ordering :: Relaxed ) ,
94107 } )
95108 }
96109}
110+
111+ fn process_chunk (
112+ lines : & [ String ] ,
113+ transforms : & [ Box < dyn Transform > ] ,
114+ deriver : & KeyDeriver ,
115+ matcher : Option < & Matcher > ,
116+ output : & dyn Output ,
117+ stats : & std:: sync:: atomic:: AtomicU64 ,
118+ matches : & std:: sync:: atomic:: AtomicU64 ,
119+ ) {
120+ lines. par_chunks ( BATCH_SIZE ) . for_each ( |batch| {
121+ let inputs: Vec < Input > = batch
122+ . iter ( )
123+ . map ( |s| Input :: from_string ( s. clone ( ) ) )
124+ . collect ( ) ;
125+ let mut buffer = Vec :: with_capacity ( inputs. len ( ) * 2 ) ;
126+
127+ for transform in transforms {
128+ buffer. clear ( ) ;
129+ transform. apply_batch ( & inputs, & mut buffer) ;
130+
131+ for ( source, key) in & buffer {
132+ let derived = deriver. derive ( key) ;
133+
134+ if let Some ( m) = matcher {
135+ if let Some ( match_info) = m. check ( & derived) {
136+ output
137+ . hit ( source, transform. name ( ) , & derived, & match_info)
138+ . ok ( ) ;
139+ matches. fetch_add ( 1 , std:: sync:: atomic:: Ordering :: Relaxed ) ;
140+ }
141+ } else {
142+ output. key ( source, transform. name ( ) , & derived) . ok ( ) ;
143+ }
144+
145+ stats. fetch_add ( 1 , std:: sync:: atomic:: Ordering :: Relaxed ) ;
146+ }
147+ }
148+ } ) ;
149+ }
150+
151+ #[ cfg( test) ]
152+ mod tests {
153+ use super :: * ;
154+ use crate :: derive:: KeyDeriver ;
155+ use crate :: output:: ConsoleOutput ;
156+ use std:: io:: Write ;
157+ use tempfile:: NamedTempFile ;
158+
159+ #[ test]
160+ fn from_file_not_found ( ) {
161+ let result = WordlistSource :: from_file ( Path :: new ( "/nonexistent/path/file.txt" ) ) ;
162+ assert ! ( result. is_err( ) ) ;
163+ }
164+
165+ #[ test]
166+ fn from_file_not_a_file ( ) {
167+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
168+ let result = WordlistSource :: from_file ( dir. path ( ) ) ;
169+ assert ! ( result. is_err( ) ) ;
170+ }
171+
172+ #[ test]
173+ fn process_empty_file ( ) {
174+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
175+ file. write_all ( b"" ) . unwrap ( ) ;
176+
177+ let source = WordlistSource :: from_file ( file. path ( ) ) . unwrap ( ) ;
178+ let deriver = KeyDeriver :: new ( ) ;
179+ let output = ConsoleOutput :: new ( ) ;
180+ let transforms: Vec < Box < dyn Transform > > = Vec :: new ( ) ;
181+
182+ let stats = source
183+ . process ( & transforms, & deriver, None , & output)
184+ . unwrap ( ) ;
185+ assert_eq ! ( stats. inputs_processed, 0 ) ;
186+ }
187+
188+ #[ test]
189+ fn process_skips_blank_lines ( ) {
190+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
191+ file. write_all ( b"hello\n \n \n world\n " ) . unwrap ( ) ;
192+
193+ let source = WordlistSource :: from_file ( file. path ( ) ) . unwrap ( ) ;
194+ let deriver = KeyDeriver :: new ( ) ;
195+ let output = ConsoleOutput :: new ( ) ;
196+ let transforms: Vec < Box < dyn Transform > > = Vec :: new ( ) ;
197+
198+ let stats = source
199+ . process ( & transforms, & deriver, None , & output)
200+ . unwrap ( ) ;
201+ assert_eq ! ( stats. inputs_processed, 2 ) ;
202+ }
203+
204+ #[ test]
205+ fn process_skips_invalid_utf8 ( ) {
206+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
207+ file. write_all ( b"valid\n \xff \xfe \n invalid bytes\n test\n " )
208+ . unwrap ( ) ;
209+
210+ let source = WordlistSource :: from_file ( file. path ( ) ) . unwrap ( ) ;
211+ let deriver = KeyDeriver :: new ( ) ;
212+ let output = ConsoleOutput :: new ( ) ;
213+ let transforms: Vec < Box < dyn Transform > > = Vec :: new ( ) ;
214+
215+ let stats = source
216+ . process ( & transforms, & deriver, None , & output)
217+ . unwrap ( ) ;
218+ assert_eq ! ( stats. inputs_processed, 3 ) ;
219+ }
220+ }
0 commit comments