File Coverage

blib/lib/Algorithm/CouponCode.pm
Criterion Covered Total %
statement 86 89 96.6
branch 21 24 87.5
condition 10 13 76.9
subroutine 12 12 100.0
pod 3 3 100.0
total 132 141 93.6


line stmt bran cond sub pod time code
1             package Algorithm::CouponCode;
2             {
3             $Algorithm::CouponCode::VERSION = '1.004';
4             }
5              
6             =head1 NAME
7              
8             Algorithm::CouponCode - Generate and validate 'CouponCode' strings
9              
10             =cut
11              
12 5     5   128406 use 5.010;
  5         26  
  5         239  
13 5     5   34 use warnings;
  5         9  
  5         177  
14 5     5   30 use strict;
  5         15  
  5         242  
15              
16              
17 5     5   29 use Exporter qw(import);
  5         11  
  5         257  
18 5     5   7195 use Digest::SHA qw(sha1);
  5         27277  
  5         8075  
19              
20              
21             our @EXPORT_OK = qw(cc_generate cc_validate make_bad_regex);
22              
23             my $sym_str = '0123456789ABCDEFGHJKLMNPQRTUVWXY';
24             my @sym = split //, $sym_str;
25             my $urandom_path = '/dev/urandom';
26             my $have_urandom = -r $urandom_path;
27             my $bad_regex = make_bad_regex();
28              
29              
30             sub cc_generate {
31 1008     1008 1 615141 my %arg = @_;
32              
33 1008   100     8903 my $parts = $arg{parts} || 3;
34 1008   66     4541 my $plaintext = $arg{plaintext} // _random_plaintext();
35 1008   66     7374 my $bad_words = $arg{bad_regex} || $bad_regex;
36 1008         1734 my($sha1_hash, @code);
37              
38 1008         18405 RANDOM_HASH: {
39 1008         1425 $sha1_hash = sha1($plaintext);
40 1008         17332 my @bytes = map { ord($_) & 31 } split //, $sha1_hash;
  20160         94907  
41 1008         10515 TRY_PART: while(@code < $parts) {
42 1029 50       12544 if(@bytes < 3) {
43 0         0 $plaintext = $sha1_hash;
44 0         0 redo RANDOM_HASH;
45             }
46 1029         2166 my $i = @code + 1;
47 1029         1894 my $str = join '', map { $sym[shift @bytes] } (0, 1, 2);
  3087         11302  
48 1029         3388 my $part = $str . _checkdigit_alg_1($str, $i);
49 1029 100       9753 next TRY_PART if $part =~ $bad_words;
50 1027 100       3282 next TRY_PART if _valid_when_swapped($part, $i);
51 1024         14408 push @code, $part;
52             }
53             }
54              
55 1008         6693 return join '-', @code;
56             }
57              
58              
59             sub cc_validate {
60 3933     3933 1 1188679 my %arg = @_;
61              
62 3933 100       14190 my $code = $arg{code} or return;
63 3932   100     11765 my $parts = $arg{parts} // 3;
64              
65 3932         6427 $code = uc($code);
66 3932         10110 $code =~ s{[^0-9A-Z]+}{}g;
67 3932         6208 $code =~ tr{OIZS}{0125};
68 3932         19043 my(@parts) = $code =~ m{([0-9A-Z]{4})}g;
69 3932 100       11897 return unless scalar(@parts) == $parts;
70              
71 3931         9920 foreach my $i (1..$parts) {
72 3990         17762 my($str, $check) = $parts[$i - 1] =~ m{^(...)(.)};
73 3990 100       10248 return unless $check eq _checkdigit_alg_1($str, $i);
74             }
75 1021         8448 return join '-', @parts;
76             }
77              
78              
79             sub make_bad_regex {
80 8     8 1 5813 my %arg = @_;
81              
82 8         30 my @word_list = _default_bad_word_list();
83 8 100       58 if($arg{words}) {
84 2 100 66     21 if($arg{mode} && $arg{mode} eq 'replace') {
85 1         3 @word_list = @{ $arg{words} };
  1         6  
86             }
87             else {
88 1         3 push @word_list, @{ $arg{words} };
  1         5  
89             }
90             }
91 200         260 my $words = join '|', map {
92 8         20 $_ = uc($_);
93 200         338 s/[I1]/[I1]/g;
94 200         318 s/[O0]/[O0]/g;
95 200         400 s/[S5]/[S5]/g;
96 200         266 s/[Z2]/[Z2]/g;
97 200         277 s/[E3]/[E3]/g;
98 200         377 s/[A4]/[A4]/g;
99 200         416 $_;
100             } @word_list;
101 8         1345 return qr{\A(?:$words)\z};
102             }
103              
104              
105             sub _default_bad_word_list {
106             # Yay for rot13
107 8     8   25 return map { my $w = $_; $w =~ tr/A-Z/N-ZA-M/; $w } qw{
  224         259  
  224         321  
  224         722  
108             SHPX PHAG JNAX JNAT CVFF PBPX FUVG GJNG GVGF SNEG URYY ZHSS QVPX XABO
109             NEFR FUNT GBFF FYHG GHEQ FYNT PENC CBBC OHGG SRPX OBBO WVFZ WVMM CUNG
110             };
111             }
112              
113              
114             sub _random_plaintext {
115 1002 50   1002   2990 if($have_urandom) {
116 1002 50       71400 open my $fh, '<', $urandom_path or die "open($urandom_path): $!";
117 1002         9351 sysread $fh, my $buf, 8;
118 1002         28108 return $buf;
119             }
120             else {
121 0         0 return $$ . localtime() . rand();
122             }
123             }
124              
125              
126             sub _checkdigit_alg_1 {
127 7997     7997   16089 my($data, $pos) = @_;
128 7997         28901 my @char = split //, $data;
129              
130 7997         41571 my $check = $pos;
131 7997         28707 foreach my $i (0..2) {
132 23991         52972 my $k = index($sym_str, $char[$i]);
133 23991         60771 $check = $check * 19 + $k;
134             }
135 7997         84870 return $sym[ $check % 31 ];
136             }
137              
138              
139             sub _valid_when_swapped {
140 1027     1027   2101 my($orig, $pos) = @_;
141              
142 1027         4694 my($a, $b, $c, $d) = split //, $orig;
143 1027         4360 foreach my $code (
144             "$b$a$c$d",
145             "$a$c$b$d",
146             "$a$b$d$c",
147             ) {
148 3077 100       11115 next if $code eq $orig;
149 2978 100       8189 if(_checkdigit_alg_1(substr($code, 0, 3), $pos) eq substr($code, 3, 1)) {
150 3         24 return 1;
151             }
152             }
153 1024         3629 return 0;
154             }
155              
156             1;
157             __END__