File Coverage

blib/lib/App/Greple/PwBlock.pm
Criterion Covered Total %
statement 24 141 17.0
branch 0 38 0.0
condition 0 19 0.0
subroutine 8 24 33.3
pod 12 15 80.0
total 44 237 18.5


line stmt bran cond sub pod time code
1             package App::Greple::PwBlock;
2              
3 15     15   98 use strict;
  15         57  
  15         579  
4 15     15   63 use warnings;
  15         46  
  15         752  
5 15     15   97 use utf8;
  15         28  
  15         87  
6              
7 15     15   600 use List::Util qw(sum reduce);
  15         25  
  15         1225  
8 15     15   83 use Data::Dumper;
  15         34  
  15         890  
9 15     15   701 use Getopt::EX::Colormap qw(colorize);
  15         92111  
  15         912  
10 15     15   7655 use Getopt::EX::Config qw(config);
  15         44272  
  15         327  
11              
12             sub new {
13 0     0 1   my $class = shift;
14 0           my $obj = bless {
15             orig => "",
16             masked => "",
17             id => {},
18             pw => {},
19             matrix => {},
20             }, $class;
21              
22 0 0         $obj->parse(shift) if @_;
23              
24 0           $obj;
25             }
26              
27             sub id {
28 0     0 1   my $obj = shift;
29 0           my $label = shift;
30 0 0         exists $obj->{id}{$label} ? $obj->{id}{$label} : undef;
31             }
32              
33             sub pw {
34 0     0 1   my $obj = shift;
35 0           my $label = shift;
36 0 0         exists $obj->{pw}{$label} ? $obj->{pw}{$label} : undef;
37             }
38              
39             sub cell {
40 0     0 1   my $obj = shift;
41 0           my($col, $row) = @_;
42 0 0         if (length $col > 1) {
43 0           ($col, $row) = split //, $col;
44             }
45 0 0         return undef if not defined $obj->{matrix}{$col};
46 0           $obj->matrix->{$col}{$row};
47             }
48              
49             sub any {
50 0     0 1   my $obj = shift;
51 0           my $label = shift;
52 0   0       $obj->id($label) // $obj->pw($label) // $obj->cell(uc $label);
      0        
53             }
54              
55 0     0 1   sub orig { $_[0]->{orig} }
56 0     0 1   sub masked { $_[0]->{masked} }
57 0     0 1   sub matrix { $_[0]->{matrix} }
58              
59              
60             sub parse {
61 0     0 1   my $obj = shift;
62 0           $obj->{orig} = $obj->{masked} = shift;
63 0 0         $obj->parse_matrix if config('parse_matrix');
64 0 0         $obj->parse_pw if config('parse_pw');
65 0 0         $obj->parse_id if config('parse_id');
66 0           $obj;
67             }
68            
69             sub make_pattern {
70 0 0   0 0   my $opt = ref $_[0] eq 'HASH' ? shift : {};
71 15     15   15158 use English;
  15         12926  
  15         81  
72 0           local $LIST_SEPARATOR = '|';
73 0           my @match = @_;
74 0           my @except = qw(INPUT);
75 0 0         push @except, @{$opt->{IGNORE}} if $opt->{IGNORE};
  0            
76 0           qr{ ^\s*+ (?!@except) .*? (?:@match)\w*[:=]? [\ \t]* \K ( .* ) }mxi;
77             }
78              
79             # Getopt::EX::Config support
80             our $config = Getopt::EX::Config->new(
81             parse_matrix => 1,
82             parse_id => 1,
83             parse_pw => 1,
84             id_keys => join(' ',
85             qw(ID ACCOUNT USER CODE NUMBER URL),
86             qw(ユーザ アカウント コード 番号),
87             ),
88             id_chars => '[\w\.\-\@]',
89             id_color => 'K/455',
90             id_label_color => 'S;C/555',
91             pw_keys => join(' ',
92             qw(PASS PIN),
93             qw(パス 暗証),
94             ),
95             pw_chars => '\S',
96             pw_color => 'K/545',
97             pw_label_color => 'S;M/555',
98             pw_blackout => 1,
99             );
100              
101             sub parse_id {
102             shift->parse_xx(
103 0     0 1   hash => 'id',
104             pattern => make_pattern(split /\s+/, config('id_keys')),
105             chars => config('id_chars'),
106             start_label => '0',
107             label_format => '[%s]',
108             color => config('id_color'),
109             label_color => config('id_label_color'),
110             blackout => 0,
111             );
112             }
113              
114             sub parse_pw {
115             shift->parse_xx(
116 0     0 1   hash => 'pw',
117             pattern => make_pattern({IGNORE => [ 'URL' ]}, split /\s+/, config('pw_keys')),
118             chars => config('pw_chars'),
119             start_label => 'a',
120             label_format => '[%s]',
121             color => config('pw_color'),
122             label_color => config('pw_label_color'),
123             blackout => config('pw_blackout'),
124             );
125             }
126              
127             sub parse_xx {
128 0     0 0   my $obj = shift;
129 0           my %opt = @_;
130 0           my %hash;
131 0           $obj->{$opt{hash}} = \%hash;
132              
133 0           my $label_id = $opt{start_label};
134 0           my $chars = qr/$opt{chars}/;
135 0           $obj->{masked} =~ s{ (?!.*\e) $opt{pattern} }{
136 0           local $_ = $1;
137 0           s{ (?| () (https?://[^\s{}|\\\^\[\]\`]+) # URL
138             | ([(]) ([^)]+)(\)) # ( text )
139             | () ($chars+) ) # text
140             }{
141 0   0       my($pre, $match, $post) = ($1, $2, $3 // '');
142 0           $hash{$label_id} = $match;
143 0           my $label = sprintf $opt{label_format}, $label_id++;
144 0 0         if ($opt{blackout}) {
145 0 0         if ($opt{blackout} > 1) {
146 0           $match = 'x' x $opt{blackout};
147             } else {
148 0   0       my $char = $opt{blackout_char} // 'x';
149 0           $match =~ s/./$char/g;
150             }
151             }
152 0 0         $label = colorize($opt{label_color}, $label) if $opt{label_color};
153 0 0         $match = colorize($opt{color}, $match) if $opt{color};
154 0           $pre . $label . $match . $post;
155             }xge;
156 0           $_;
157             }igex;
158              
159 0           $obj;
160             }
161              
162             sub parse_matrix {
163 0     0 1   my $obj = shift;
164 0           my @area = guess_matrix_area($obj->{masked});
165 0           my %matrix;
166 0           $obj->{matrix} = \%matrix;
167              
168 0           for my $area (@area) {
169 0           my $start = $area->[0];
170 0           my $len = $area->[1] - $start;
171 0           my $matrix = substr($obj->{masked}, $start, $len);
172 0           $matrix =~ s{ \b (?\d) \W+ \K (?.*) $}{
173 0           my $index = $+{index};
174 0           my $chars = $+{chars};
175 0           my $col = 'A';
176 0           $chars =~ s{(\S+)}{
177 0           my $cell = $1;
178 0           $matrix{$col}{$index} = $cell;
179 0           $col++;
180 0           $cell =~ s/./x/g;
181 0           colorize('D;R', $cell);
182             }ge;
183 0           $chars;
184             }xmge;
185 0           substr($obj->{masked}, $start, $len) = $matrix;
186 0           last; # process 1st segment only
187             }
188              
189 0           $obj;
190             }
191            
192             sub guess_matrix_area {
193 0     0 0   my $text = shift;
194 0           my @text = $text =~ /(.*\n|.+\z)/g;
195 0           my @length = map { length } @text;
  0            
196 0           my @words = map { [ /(\w+)/g ] } @text;
  0            
197 0           my @one = map { [ grep { length == 1 } @$_ ] } @words;
  0            
  0            
198 0           my @two = map { [ grep { length == 2 } @$_ ] } @words;
  0            
  0            
199 0           my @more = map { [ grep { length >= 3 } @$_ ] } @words;
  0            
  0            
200 0           my $series = 5;
201              
202 0           map { [ sum(@length[0 .. $_->[0]]) - $length[$->[0]],
203             sum(@length[0 .. $_->[1]]) ] }
204 0           sort { $b->[1] - $b->[0] <=> $a->[1] - $a->[0] }
205 0           grep { $_->[0] + $series - 1 <= $_->[1] }
206 0 0         map { defined $_ ? ref $_ ? @$_ : [$_, $_] : () }
    0          
207             reduce {
208 0 0   0     my $r = ref $a eq 'ARRAY' ? $a : [ [$a, $a] ];
209 0           my $l = $r->[-1][1];
210 0 0 0       if ($l + 1 == $b
      0        
211 0           and @{$one[$l]} == @{$one[$b]}
  0            
212 0           and @{$two[$l]} == @{$two[$b]}
  0            
213             ) {
214 0           $r->[-1][1] = $b;
215             } else {
216 0           push @$r, [ $b, $b ];
217             }
218 0           $r;
219             }
220 0           grep { $one[$_][0] =~ /\d/ }
221 0 0 0       grep { @{$one[$_]} >= 10 || @{$two[$_]} >= 5 and @{$more[$_]} == 0 }
  0            
  0            
  0            
  0            
222             0 .. $#text;
223             }
224              
225             1;
226              
227             =encoding utf-8
228              
229             =head1 NAME
230              
231             App::Greple::PwBlock - Password and ID information block parser for greple
232              
233             =head1 SYNOPSIS
234              
235             use App::Greple::PwBlock;
236            
237             # Create a new PwBlock object
238             my $pb = App::Greple::PwBlock->new($text);
239            
240             # Access parsed information
241             my $id = $pb->id('0'); # Get ID by label
242             my $pw = $pb->pw('a'); # Get password by label
243             my $cell = $pb->cell('A', 0); # Get matrix cell value
244            
245             # Configuration
246             use App::Greple::PwBlock qw(config);
247             config('id_keys', 'LOGIN EMAIL USER ACCOUNT');
248             config('pw_blackout', 0);
249              
250             =head1 DESCRIPTION
251              
252             B is a specialized parser for extracting and managing
253             password and ID information from text blocks. It provides intelligent
254             pattern recognition for common credential formats and includes support
255             for random number matrices used by banking systems.
256              
257             The module uses L for centralized parameter management,
258             allowing configuration of parsing behavior, display colors, and keyword
259             patterns.
260              
261             =head1 METHODS
262              
263             =over 4
264              
265             =item B([I])
266              
267             Creates a new PwBlock object. If I is provided, it will be parsed
268             immediately.
269              
270             my $pb = App::Greple::PwBlock->new($credential_text);
271              
272             =item B(I)
273              
274             Parses the given text to extract ID, password, and matrix information.
275             This method is called automatically by B if text is provided.
276              
277             $pb->parse($text);
278              
279             =item B(I
280              
281             Returns the ID value associated with the given label. Labels are assigned
282             automatically during parsing (e.g., '0', '1', '2', ...).
283              
284             my $username = $pb->id('0');
285              
286             =item B(I
287              
288             Returns the password value associated with the given label. Labels are
289             assigned automatically during parsing (e.g., 'a', 'b', 'c', ...).
290              
291             my $password = $pb->pw('a');
292              
293             =item B(I, I)
294              
295             Returns the value from a matrix cell at the specified column and row.
296             Useful for banking security matrices.
297              
298             my $value = $pb->cell('E', 3); # Column E, Row 3
299              
300             =item B(I
301              
302             Returns any value (ID, password, or matrix cell) associated with the label.
303             This is a convenient method that checks all types.
304              
305             my $value = $pb->any('a');
306              
307             =item B()
308              
309             Returns the original unparsed text.
310              
311             =item B()
312              
313             Returns the text with passwords masked according to the B setting.
314              
315             =item B()
316              
317             Returns a hash reference containing the parsed matrix data.
318              
319             =back
320              
321             =head1 CONFIGURATION
322              
323             This module uses L for parameter management. Configuration
324             can be accessed using the B function:
325              
326             use App::Greple::PwBlock qw(config);
327              
328             =head2 Available Parameters
329              
330             =over 4
331              
332             =item B (boolean, default: 1)
333              
334             Enable or disable matrix parsing.
335              
336             =item B (boolean, default: 1)
337              
338             Enable or disable ID field parsing.
339              
340             =item B (boolean, default: 1)
341              
342             Enable or disable password field parsing.
343              
344             =item B (string, default: "ID ACCOUNT USER CODE NUMBER URL ユーザ アカウント コード 番号")
345              
346             Space-separated list of keywords that identify ID fields.
347              
348             =item B (string, default: "[\w\.\-\@]")
349              
350             Regular expression character class for valid ID characters.
351              
352             =item B (string, default: "K/455")
353              
354             Color specification for ID values in output.
355              
356             =item B (string, default: "S;C/555")
357              
358             Color specification for ID labels in output.
359              
360             =item B (string, default: "PASS PIN パス 暗証")
361              
362             Space-separated list of keywords that identify password fields.
363              
364             =item B (string, default: "\S")
365              
366             Regular expression character class for valid password characters.
367              
368             =item B (string, default: "K/545")
369              
370             Color specification for password values in output.
371              
372             =item B (string, default: "S;M/555")
373              
374             Color specification for password labels in output.
375              
376             =item B (boolean, default: 1)
377              
378             When enabled, passwords are masked in the output for security.
379              
380             =back
381              
382             =head2 Configuration Examples
383              
384             # Customize ID keywords
385             config('id_keys', 'LOGIN EMAIL USERNAME ACCOUNT');
386            
387             # Disable password masking
388             config('pw_blackout', 0);
389            
390             # Add custom password keywords
391             config('pw_keys', 'PASS PASSWORD PIN SECRET TOKEN');
392              
393             =head1 MATRIX SUPPORT
394              
395             The module can automatically detect and parse random number matrices
396             commonly used by banking systems for security:
397              
398             | A B C D E F G H I J
399             --+--------------------
400             0 | Y W 0 B 8 P 4 C Z H
401             1 | M 0 6 I K U C 8 6 Z
402             2 | 7 N R E Y 1 9 3 G 5
403              
404             Access matrix values using:
405              
406             my $value = $pb->cell('E', 3); # Gets the value at column E, row 3
407              
408             =head1 SEE ALSO
409              
410             L, L
411              
412             =head1 AUTHOR
413              
414             Kazumasa Utashiro
415              
416             =head1 LICENSE
417              
418             Copyright (C) 2017-2025 Kazumasa Utashiro.
419              
420             This library is free software; you can redistribute it and/or modify
421             it under the same terms as Perl itself.
422              
423             =cut