File Coverage

blib/lib/App/Genpass.pm
Criterion Covered Total %
statement 86 124 69.3
branch 15 40 37.5
condition 11 27 40.7
subroutine 13 16 81.2
pod 4 4 100.0
total 129 211 61.1


line stmt bran cond sub pod time code
1             package App::Genpass;
2             # ABSTRACT: Quickly and easily create secure passwords
3             $App::Genpass::VERSION = '2.34';
4 5     5   3981 use Carp;
  5         8  
  5         427  
5 5     5   5334 use Moo;
  5         503728  
  5         40  
6 5     5   16246 use Sub::Quote 'quote_sub';
  5         22336  
  5         360  
7 5     5   5677 use MooX::Types::MooseLike::Base qw/Int Str Bool ArrayRef/;
  5         41542  
  5         640  
8 5     5   6773 use Getopt::Long qw/:config no_ignore_case/;
  5         79454  
  5         34  
9 5     5   1033 use File::Spec;
  5         9  
  5         101  
10 5     5   4392 use Config::Any;
  5         69203  
  5         194  
11 5     5   5164 use File::HomeDir;
  5         39122  
  5         633  
12 5     5   4914 use List::AllUtils qw( any none shuffle );
  5         15315  
  5         21604  
13              
14             has uppercase => (
15             is => 'ro',
16             isa => ArrayRef,
17             default => quote_sub( q{ [ 'A' .. 'Z' ] } ),
18             );
19              
20             has lowercase => (
21             is => 'ro',
22             isa => ArrayRef,
23             default => quote_sub( q{ [ 'a' .. 'z' ] } ),
24             );
25              
26             has numerical => (
27             is => 'ro',
28             isa => ArrayRef,
29             default => quote_sub( q{ [ '0' .. '9' ] } ),
30             );
31              
32             has unreadable => (
33             is => 'ro',
34             isa => ArrayRef,
35             default => quote_sub( q{ [ split //sm, q{oO0l1I} ] } ),
36             );
37              
38             has specials => (
39             is => 'ro',
40             isa => ArrayRef,
41             default => quote_sub( q{ [ split //sm, q{!@#$%^&*()} ] } ),
42             );
43              
44             has number => (
45             is => 'ro',
46             isa => Int,
47             default => quote_sub( q{1} ),
48             );
49              
50             has readable => (
51             is => 'ro',
52             isa => Bool,
53             default => quote_sub( q{1} ),
54             );
55              
56             has verify => (
57             is => 'ro',
58             isa => Bool,
59             default => quote_sub( q{1} ),
60             );
61              
62             has length => (
63             is => 'ro',
64             isa => Int,
65             );
66              
67             has minlength => (
68             is => 'rw',
69             isa => Int,
70             default => quote_sub( q{8} ),
71             );
72              
73             has maxlength => (
74             is => 'rw',
75             isa => Int,
76             default => quote_sub( q{10} ),
77             );
78              
79             sub parse_opts {
80 0     0 1 0 my $class = shift;
81 0         0 my %opts = ();
82              
83 0 0       0 GetOptions(
84             'configfile=s' => \$opts{'configfile'},
85             'lowercase=s@' => \$opts{'lowercase'},
86             'uppercase=s@' => \$opts{'uppercase'},
87             'numerical=i@' => \$opts{'numerical'},
88             'unreadable=s@' => \$opts{'unreadable'},
89             'specials=s@' => \$opts{'specials'},
90             'n|number=i' => \$opts{'number'},
91             'r|readable!' => \$opts{'readable'},
92             'v|verify!' => \$opts{'verify'},
93             'l|length=i' => \$opts{'length'},
94             'm|minlength=i' => \$opts{'minlength'},
95             'x|maxlength=i' => \$opts{'maxlength'},
96             ) or croak q{Can't get options.};
97              
98             # remove undefined keys
99 0         0 foreach my $key ( keys %opts ) {
100 0 0       0 defined $opts{$key} or delete $opts{$key};
101             }
102              
103 0         0 return %opts;
104             }
105              
106             sub new_with_options {
107 0     0 1 0 my $class = shift;
108 0         0 my %opts = $class->parse_opts;
109 0         0 my @configs = (
110             File::Spec->catfile( File::HomeDir->my_home, '.genpass.yaml' ),
111             '/etc/genpass.yaml',
112             );
113              
114 0 0       0 if ( ! exists $opts{'configfile'} ) {
115 0         0 foreach my $file (@configs) {
116 0 0 0     0 if ( -e $file && -r $file ) {
117 0         0 $opts{'configfile'} = $file;
118 0         0 last;
119             }
120             }
121             }
122              
123 0 0       0 if ( exists $opts{'configfile'} ) {
124 0         0 %opts = (
125             %opts,
126 0         0 %{ $class->get_config_from_file( $opts{'configfile'} ) },
127             );
128             }
129              
130 0         0 my $self = $class->new( %opts, @_ );
131              
132 0         0 return $self;
133             }
134              
135             sub get_config_from_file {
136 0     0 1 0 my ($class, $file) = @_;
137              
138 0 0       0 $file = $file->() if ref $file eq 'CODE';
139 0 0       0 my $files_ref = ref $file eq 'ARRAY' ? $file : [$file];
140              
141 0         0 my $can_config_any_args = $class->can('config_any_args');
142 0 0       0 my $extra_args = $can_config_any_args ?
143             $can_config_any_args->($class, $file) : {};
144             ;
145 0         0 my $raw_cfany = Config::Any->load_files({
146             %$extra_args,
147             use_ext => 1,
148             files => $files_ref,
149             flatten_to_hash => 1,
150             } );
151              
152 0         0 my %raw_config;
153 0         0 foreach my $file_tested ( reverse @{$files_ref} ) {
  0         0  
154 0 0       0 if ( ! exists $raw_cfany->{$file_tested} ) {
155 0         0 warn qq{Specified configfile '$file_tested' does not exist, } .
156             qq{is empty, or is not readable\n};
157 0         0 next;
158             }
159              
160 0         0 my $cfany_hash = $raw_cfany->{$file_tested};
161 0 0 0     0 die "configfile must represent a hash structure in file: $file_tested"
      0        
162             unless $cfany_hash && ref $cfany_hash && ref $cfany_hash eq 'HASH';
163              
164 0         0 %raw_config = ( %raw_config, %{$cfany_hash} );
  0         0  
165             }
166              
167 0         0 \%raw_config;
168             }
169              
170             sub _get_chars {
171 5010     5010   17609 my $self = shift;
172 5010         11703 my @all_types = qw( lowercase uppercase numerical specials );
173 5010         5790 my @chars = ();
174 5010         5582 my @types = ();
175              
176             # adding all the combinations
177 5010         7525 foreach my $type (@all_types) {
178 20040 50       53277 if ( my $ref = $self->$type ) {
179 20040         19369 push @chars, @{$ref};
  20040         88468  
180 20040         42355 push @types, $type;
181             }
182             }
183              
184             # removing the unreadable chars
185 5010 100       14228 if ( $self->readable ) {
186 5007         9945 my @remove_chars = (
187 5007         23315 @{ $self->unreadable },
188 5007         5702 @{ $self->specials },
189             );
190              
191 360368         485963 @chars = grep {
192 5007         8989 local $a = $_;
193 360368     5165216   1038611 none { $a eq $_ } @remove_chars;
  5165216         5932493  
194             } @chars;
195              
196             # removing specials
197 5007         25979 pop @types;
198             }
199              
200             # make both refs
201 5010         118026 return [ \@types, @chars ];
202             }
203              
204             sub generate {
205 5008     5008 1 82764 my ( $self, $number ) = @_;
206              
207 5008         6032 my $length;
208 5008         9734 my $verify = $self->verify;
209 5008         7048 my @passwords = ();
210 5008         5593 my @verifications = ();
211 5008         5795 my $EMPTY = q{};
212              
213 5008         4711 my ( $char_types, @chars ) = @{ $self->_get_chars };
  5008         10149  
214              
215 5008         27692 my @char_types = @{$char_types};
  5008         11268  
216 5008         7457 my $num_of_types = scalar @char_types;
217              
218 5008 50 66     169579 if ( (defined($self->length) && $num_of_types > $self->length)
      33        
219             || ($num_of_types > $self->minlength) ) {
220 0 0       0 $length = defined($self->length) ? $self->length : $self->minlength.' minimum';
221 0         0 croak <<"_DIE_MSG";
222             You wanted a shorter password that the variety of characters you've selected.
223             You requested $num_of_types types of characters but only have $length length.
224             _DIE_MSG
225             }
226              
227 5008 100       149191 if ($self->minlength > $self->maxlength) {
228 1         212 carp "minlength > maxlength, so I'm switching them";
229 1         152 my $min = $self->maxlength;
230 1         22 $self->maxlength($self->minlength);
231 1         91 $self->minlength($min);
232             }
233              
234 5008   66     225500 $length = $self->length
235             || $self->minlength + int(rand(abs($self->maxlength - $self->minlength) + 1));
236              
237 5008   66     203894 $number ||= $self->number;
238              
239             # each password iteration needed
240 5008         10189 foreach my $pass_iter ( 1 .. $number ) {
241 5044         6593 my $password = $EMPTY;
242 5044         7143 my $char_type = shift @char_types;
243              
244             # generating the password
245 5044         10791 while ( $length > length $password ) {
246 42292         75066 my $char = $chars[ int rand @chars ];
247              
248             # for verifying, we just check that it has small capital letters
249             # if that doesn't work, we keep asking it to get a new random one
250             # the check if it has large capital letters and so on
251 42292 100 66     152704 if ( $verify && $char_type && @{ $self->$char_type } ) {
  15143   66     73464  
252             # verify $char_type
253 15143 50       14123 if ( @{ $self->$char_type } ) {
  15143         37104  
254 15143     821971   41144 while ( ! any { $_ eq $char } @{ $self->$char_type } ) {
  821971         912624  
  59057         187631  
255 43914         177887 $char = $chars[ int rand @chars ];
256             }
257             }
258              
259             $char_type =
260 15143 100       51183 scalar @char_types > 0 ? shift @char_types : $EMPTY;
261             }
262              
263 42292         83457 $password .= $char;
264             }
265              
266             # since the verification process creates a situation of ordered types
267             # (lowercase, uppercase, numerical, special)
268             # we need to shuffle the string
269 5044         25173 $password = join $EMPTY, shuffle( split //sm, $password );
270              
271 5044 100       50640 $number == 1 && return $password;
272              
273 40         54 push @passwords, $password;
274              
275 40         40 @char_types = @{$char_types};
  40         102  
276             }
277              
278 4 100       51 return wantarray ? @passwords : \@passwords;
279             }
280              
281              
282             1;
283              
284             __END__