File Coverage

blib/lib/Algorithm/CouponCode.pm
Criterion Covered Total %
statement 85 88 96.5
branch 21 24 87.5
condition 10 13 76.9
subroutine 12 12 100.0
pod 3 3 100.0
total 131 140 93.5


line stmt bran cond sub pod time code
1             package Algorithm::CouponCode;
2             $Algorithm::CouponCode::VERSION = '1.005';
3             =head1 NAME
4              
5             Algorithm::CouponCode - Generate and validate 'CouponCode' strings
6              
7             =cut
8              
9 5     5   108796 use 5.010;
  5         20  
10 5     5   25 use warnings;
  5         10  
  5         139  
11 5     5   25 use strict;
  5         14  
  5         128  
12              
13              
14 5     5   25 use Exporter qw(import);
  5         8  
  5         169  
15 5     5   4336 use Digest::SHA qw(sha1);
  5         19642  
  5         6052  
16              
17              
18             our @EXPORT_OK = qw(cc_generate cc_validate make_bad_regex);
19              
20             my $sym_str = '0123456789ABCDEFGHJKLMNPQRTUVWXY';
21             my @sym = split //, $sym_str;
22             my $urandom_path = '/dev/urandom';
23             my $have_urandom = -r $urandom_path;
24             my $bad_regex = make_bad_regex();
25              
26              
27             sub cc_generate {
28 1008     1008 1 389906 my %arg = @_;
29              
30 1008   100     3295 my $parts = $arg{parts} || 3;
31 1008   66     3511 my $plaintext = $arg{plaintext} // _random_plaintext();
32 1008   66     5263 my $bad_words = $arg{bad_regex} || $bad_regex;
33 1008         1602 my($sha1_hash, @code);
34              
35             RANDOM_HASH: {
36 1008         1422 $sha1_hash = sha1($plaintext);
  1008         7439  
37 1008         9590 my @bytes = map { ord($_) & 31 } split //, $sha1_hash;
  20160         62961  
38 1008         8897 TRY_PART: while(@code < $parts) {
39 1030 50       2890 if(@bytes < 3) {
40 0         0 $plaintext = $sha1_hash;
41 0         0 redo RANDOM_HASH;
42             }
43 1030         1945 my $i = @code + 1;
44 1030         1780 my $str = join '', map { $sym[shift @bytes] } (0, 1, 2);
  3090         8986  
45 1030         2695 my $part = $str . _checkdigit_alg_1($str, $i);
46 1030 100       7351 next TRY_PART if $part =~ $bad_words;
47 1028 100       2073 next TRY_PART if _valid_when_swapped($part, $i);
48 1024         6226 push @code, $part;
49             }
50             }
51              
52 1008         5127 return join '-', @code;
53             }
54              
55              
56             sub cc_validate {
57 3937     3937 1 752444 my %arg = @_;
58              
59 3937 100       12849 my $code = $arg{code} or return;
60 3936   100     10677 my $parts = $arg{parts} // 3;
61              
62 3936         6529 $code = uc($code);
63 3936         9067 $code =~ s{[^0-9A-Z]+}{}g;
64 3936         6532 $code =~ tr{OIZS}{0125};
65 3936         16806 my(@parts) = $code =~ m{([0-9A-Z]{4})}g;
66 3936 100       11285 return unless scalar(@parts) == $parts;
67              
68 3935         9133 foreach my $i (1..$parts) {
69 3994         16834 my($str, $check) = $parts[$i - 1] =~ m{^(...)(.)};
70 3994 100       10689 return unless $check eq _checkdigit_alg_1($str, $i);
71             }
72 1021         6908 return join '-', @parts;
73             }
74              
75              
76             sub make_bad_regex {
77 8     8 1 5432 my %arg = @_;
78              
79 8         24 my @word_list = _default_bad_word_list();
80 8 100       49 if($arg{words}) {
81 2 100 66     21 if($arg{mode} && $arg{mode} eq 'replace') {
82 1         2 @word_list = @{ $arg{words} };
  1         9  
83             }
84             else {
85 1         2 push @word_list, @{ $arg{words} };
  1         6  
86             }
87             }
88             my $words = join '|', map {
89 8         22 $_ = uc($_);
  200         286  
90 200         371 s/[I1]/[I1]/g;
91 200         347 s/[O0]/[O0]/g;
92 200         380 s/[S5]/[S5]/g;
93 200         348 s/[Z2]/[Z2]/g;
94 200         308 s/[E3]/[E3]/g;
95 200         392 s/[A4]/[A4]/g;
96 200         396 $_;
97             } @word_list;
98 8         1160 return qr{\A(?:$words)\z};
99             }
100              
101              
102             sub _default_bad_word_list {
103             # Yay for rot13
104 8     8   24 return map { my $w = $_; $w =~ tr/A-Z/N-ZA-M/; $w } qw{
  224         297  
  224         288  
  224         514  
105             SHPX PHAG JNAX JNAT CVFF PBPX FUVG GJNG GVGF SNEG URYY ZHSS QVPX XABO
106             NEFR FUNT GBFF FYHG GHEQ FYNT PENC CBBC OHGG SRPX OBBO WVFZ WVMM CUNG
107             };
108             }
109              
110              
111             sub _random_plaintext {
112 1002 50   1002   2421 if($have_urandom) {
113 1002 50       27988 open my $fh, '<', $urandom_path or die "open($urandom_path): $!";
114 1002         6591 sysread $fh, my $buf, 8;
115 1002         12293 return $buf;
116             }
117             else {
118 0         0 return $$ . localtime() . rand();
119             }
120             }
121              
122              
123             sub _checkdigit_alg_1 {
124 8008     8008   15952 my($data, $pos) = @_;
125 8008         19772 my @char = split //, $data;
126              
127 8008         11574 my $check = $pos;
128 8008         15603 foreach my $i (0..2) {
129 24024         39567 my $k = index($sym_str, $char[$i]);
130 24024         46371 $check = $check * 19 + $k;
131             }
132 8008         55779 return $sym[ $check % 31 ];
133             }
134              
135              
136             sub _valid_when_swapped {
137 1028     1028   1785 my($orig, $pos) = @_;
138              
139 1028         3311 my($a, $b, $c, $d) = split //, $orig;
140 1028         3756 foreach my $code (
141             "$b$a$c$d",
142             "$a$c$b$d",
143             "$a$b$d$c",
144             ) {
145 3078 100       7272 next if $code eq $orig;
146 2984 100       7607 if(_checkdigit_alg_1(substr($code, 0, 3), $pos) eq substr($code, 3, 1)) {
147 4         26 return 1;
148             }
149             }
150 1024         2974 return 0;
151             }
152              
153             1;
154             __END__