File Coverage

blib/lib/Test/Mojibake.pm
Criterion Covered Total %
statement 171 181 94.4
branch 94 104 90.3
condition 21 21 100.0
subroutine 14 14 100.0
pod 3 3 100.0
total 303 323 93.8


line stmt bran cond sub pod time code
1             package Test::Mojibake;
2             # ABSTRACT: check your source for encoding misbehavior.
3              
4              
5 13     13   166878 use strict;
  13         18  
  13         331  
6 13     13   43 use warnings qw(all);
  13         12  
  13         462  
7              
8             our $VERSION = '1.3'; # VERSION
9              
10 13     13   5428 use File::Spec::Functions;
  13         7044  
  13         881  
11 13     13   1780 use Test::Builder;
  13         21709  
  13         1565  
12              
13             my %ignore_dirs = (
14             '.bzr' => 'Bazaar',
15             '.git' => 'Git',
16             '.hg' => 'Mercurial',
17             '.pc' => 'quilt',
18             '.svn' => 'Subversion',
19             CVS => 'CVS',
20             RCS => 'RCS',
21             SCCS => 'SCCS',
22             _darcs => 'darcs',
23             _sgbak => 'Vault/Fortress',
24             );
25              
26             my $Test = Test::Builder->new;
27              
28             # Use a faster/safer XS alternative, if present
29              
30             ## no critic (ProhibitStringyEval, RequireCheckingReturnValueOfEval, ProhibitPackageVars)
31             our ($use_xs, $use_pp) = (0, 0);
32             if ( eval 'require Unicode::CheckUTF8' ) {
33             $use_xs = 1;
34             }
35             elsif ( eval 'require Unicode::CheckUTF8::PP' ) {
36             $use_pp = 1;
37             }
38              
39             sub import {
40 13     13   82 my ($self, @args) = @_;
41 13         19 my $caller = caller;
42              
43 13         25 for my $func (qw(file_encoding_ok all_files all_files_encoding_ok)) {
44             ## no critic (ProhibitNoStrict)
45 13     13   50 no strict 'refs';
  13         14  
  13         12719  
46 39         47 *{$caller."::".$func} = \&$func;
  39         132  
47             }
48              
49 13         41 $Test->exported_to($caller);
50 13         82 $Test->plan(@args);
51              
52 13         3796 return;
53             }
54              
55              
56             ## no critic (ProhibitCascadingIfElse, ProhibitExcessComplexity)
57             sub file_encoding_ok {
58 34     34 1 387604 my ($file, $name) = @_;
59 34 100       113 $name = defined($name) ? $name : "Mojibake test for $file";
60              
61             ## no critic (ProhibitFiletest_f)
62 34 100       513 unless (-f $file) {
63 1         3 $Test->ok(0, $name);
64 1         321 $Test->diag("$file does not exist");
65 1         44 return;
66             }
67              
68 33         34 my $fh;
69 33 50       839 unless (open($fh, '<:raw', $file)) {
70 0         0 close $fh;
71 0         0 $Test->ok(0, $name);
72 0         0 $Test->diag("Can't open $file: $!");
73 0         0 return;
74             }
75              
76 33         53 my $use_utf8 = 0;
77 33         32 my $pod = 0;
78 33         27 my $pod_utf8 = 0;
79 33         34 my $n = 1;
80 33         53 my %pod = ();
81 33         425 while (my $line = <$fh>) {
82 2635 100 100     9302 if (($n == 1) && $line =~ /^\x{EF}\x{BB}\x{BF}/x) {
    100          
    100          
    100          
    100          
83 2         8 $Test->ok(0, $name);
84 2         774 $Test->diag("UTF-8 BOM (Byte Order Mark) found in $file");
85 2         111 return;
86             } elsif ($line =~ /^=+cut\s*$/x) {
87 11         12 $pod = 0;
88             } elsif ($line =~ /^=+encoding\s+([\w\-]+)/x) {
89 14         43 my $pod_encoding = lc $1;
90 14         25 $pod_encoding =~ y/-//d;
91              
92             # perlpod states:
93             # =encoding affects the whole document, and must occur only once.
94 14         58 ++$pod{$pod_encoding};
95 14 100       44 if (1 < scalar keys %pod) {
96 2         7 $Test->ok(0, $name);
97 2         673 $Test->diag("POD =encoding redeclared in $file, line $n");
98 2         113 return;
99             }
100              
101 12 100       27 $pod_utf8 = ($pod_encoding eq 'utf8') ? 1 : 0;
102 12         17 $pod = 1;
103             } elsif ($line =~ /^=+\w+/x) {
104 216         220 $pod = 1;
105             } elsif ($pod == 0) {
106             # source
107 1185         1201 $line =~ s/^\s*\#.*$//sx; # disclaimers placed in headers frequently contain UTF-8 *before* its usage is declared.
108 1185         2372 foreach (split m{;}x, $line) {
109             # trim
110 1655         5973 s/^\s+|\s+$//gsx;
111              
112 1655         2170 my @type = qw(0 0 0);
113 1655         2014 ++$type[_detect_utf8(\$_)];
114 1655         1774 my ($latin1, $utf8) = @type[0, 2];
115              
116 1655 100       3710 if (/^use\s+utf8(?:::all)?$/x) {
    100          
    100          
117 6         8 $use_utf8 = 1;
118             } elsif (/^use\s+common::sense$/x) {
119 1         2 $use_utf8 = 1;
120             } elsif (/^no\s+utf8$/x) {
121 2         3 $use_utf8 = 0;
122             }
123              
124 1655 100 100     7507 if (($use_utf8 == 0) && $utf8) {
    100 100        
125 2         7 $Test->ok(0, $name);
126 2         724 $Test->diag("UTF-8 unexpected in $file, line $n (source)");
127 2         117 return;
128             } elsif (($use_utf8 == 1) && $latin1) {
129 2         9 $Test->ok(0, $name);
130 2         893 $Test->diag("Non-UTF-8 unexpected in $file, line $n (source)");
131 2         119 return;
132             }
133             }
134             } else {
135             # POD
136 1207         1386 my @type = qw(0 0 0);
137 1207         1263 ++$type[_detect_utf8(\$line)];
138 1207         1297 my ($latin1, $utf8) = @type[0, 2];
139              
140 1207 100 100     4994 if (($pod_utf8 == 0) && $utf8) {
    100 100        
141 2         21 $Test->ok(0, $name);
142 2         1085 $Test->diag("UTF-8 unexpected in $file, line $n (POD)");
143 2         175 return;
144             } elsif (($pod_utf8 == 1) && $latin1) {
145 2         9 $Test->ok(0, $name);
146 2         681 $Test->diag("Non-UTF-8 unexpected in $file, line $n (POD)");
147 2         113 return;
148             }
149             }
150             } continue {
151 2623         6797 ++$n;
152             }
153 21         161 close $fh;
154              
155 21         89 $Test->ok(1, $name);
156 21         5782 return 1;
157             }
158              
159              
160             sub all_files_encoding_ok {
161 5     5 1 294489 my (@args) = @_;
162 5 100       26 @args = _starting_points() unless @args;
163              
164             ## no critic (ProhibitFiletest_f)
165 5 100       12 my @files = map { -d $_ ? all_files($_) : (-f $_ ? $_ : ()) } @args;
  13 100       276  
166              
167 5 100       23 unless (@files) {
168 1         4 $Test->plan(skip_all => 'could not find any files to test');
169 1         5 return;
170             }
171              
172 4         31 $Test->plan(tests => scalar @files);
173              
174 4         669 my $ok = 1;
175 4         13 foreach my $file (@files) {
176 20 100       40 file_encoding_ok($file) or undef $ok;
177             }
178 4         94 return $ok;
179             }
180              
181              
182             sub all_files {
183 3     3 1 75933 my (@queue) = @_;
184 3 100       15 @queue = _starting_points() unless @queue;
185 3         8 my @mod = ();
186              
187 3         10 while (@queue) {
188 59         75 my $file = shift @queue;
189 59 100       401 if (-d $file) {
190 29 50       435 opendir my $dh, $file or next;
191 29         216 my @newfiles = readdir $dh;
192 29         145 closedir $dh;
193              
194 29         66 @newfiles = no_upwards(@newfiles);
195 29         310 @newfiles = grep { not exists $ignore_dirs{$_} } @newfiles;
  56         115  
196              
197 29         43 foreach my $newfile (@newfiles) {
198 56         193 my $filename = catfile($file, $newfile);
199 56 100       509 unless (-d $filename) {
200 30         93 push @queue, $filename;
201             } else {
202 26         106 push @queue, catdir($file, $newfile);
203             }
204             }
205             }
206              
207             ## no critic (ProhibitFiletest_f)
208 59 100       388 if (-f $file) {
209 30 100       44 push @mod, $file if _is_perl($file);
210             }
211             }
212 3         21 return @mod;
213             }
214              
215             sub _starting_points {
216 2 50   2   35 return 'blib' if -e 'blib';
217 0         0 return 'lib';
218             }
219              
220             sub _is_perl {
221 30     30   37 my $file = shift;
222              
223 30 100       64 return 1 if $file =~ /\.PL$/x;
224 29 100       86 return 1 if $file =~ /\.p(?:l|m|od)$/x;
225 22 50       33 return 1 if $file =~ /\.t$/x;
226              
227 22 50       424 open my $fh, '<', $file or return;
228 22         162 my $first = <$fh>;
229 22         83 close $fh;
230              
231 22 100 100     120 return 1 if defined $first && ($first =~ /(?:^\#!.*perl)|--\*-Perl-\*--/x);
232              
233 17         82 return;
234             }
235              
236              
237             sub _detect_utf8 {
238              
239 13     13   5910 use bytes;
  13         98  
  13         49  
240 13     13   5582 use integer;
  13         102  
  13         46  
241              
242 109904     109904   1343348 my $str = shift;
243              
244 109904 100       103877 if ($use_xs) {
    50          
245 108072 100       60187 if (Unicode::CheckUTF8::is_utf8(${$str})) {
  108072         274626  
246 72990 100       47187 return (${$str} =~ m{[\x{80}-\x{ff}]}x) ? 2 : 1
  72990         163874  
247             } else {
248 35082         46045 return 0;
249             }
250             } elsif ($use_pp) {
251 0 0       0 if (Unicode::CheckUTF8::PP::is_utf8(${$str})) {
  0         0  
252 0 0       0 return (${$str} =~ m{[\x{80}-\x{ff}]}x) ? 2 : 1
  0         0  
253             } else {
254 0         0 return 0;
255             }
256             }
257              
258 1832         1207 my $d = 0;
259 1832         1149 my $c = 0;
260 1832         1191 my $bv = 0;
261 1832         1074 my $bits = 0;
262 1832         1118 my $len = length ${$str};
  1832         2428  
263              
264 1832         3140 for (my $i = 0; $i < $len; $i++) {
265 276381         175638 $c = ord(substr(${$str}, $i, 1));
  276381         257987  
266 276381 100       688091 if ($c >= 128) {
    100          
267 9100         6473 $d++;
268              
269 9100 100       27484 if ($c >= 254) {
    100          
    100          
    100          
    100          
    100          
270 1         9 return 0;
271             } elsif ($c >= 252) {
272 1         2 $bits = 6;
273             } elsif ($c >= 248) {
274 1         2 $bits = 5;
275             } elsif ($c >= 240) {
276 1         2 $bits = 4;
277             } elsif ($c >= 224) {
278 607         529 $bits = 3;
279             } elsif ($c >= 192) {
280 8488         6631 $bits = 2;
281             } else {
282 1         5 return 0;
283             }
284              
285 9098 100       12618 if (($i + $bits) > $len) {
286 1         4 return 0;
287             }
288              
289 9097         15413 my @buf = ((0) x 4, $c & ((1 << (7 - $bits)) - 1));
290 9097         11049 while ($bits > 1) {
291 9107         5174 $i++;
292 9107         5448 $bv = ord(substr(${$str}, $i, 1));
  9107         10498  
293 9107 100 100     28533 if (($bv < 128) || ($bv > 191)) {
294 607         1606 return 0;
295             }
296 8500         13305 $buf[7 - $bits] = $bv & 0x3f;
297 8500         13230 $bits--;
298             }
299 8490 100       28482 return 0 if "\0\0\0\0\0\x2f" eq pack 'c6', @buf;
300             } elsif ($c == 0) {
301 2         6 return 0;
302             }
303             }
304              
305 1215 100       2773 return $d ? 2 : 1;
306             }
307              
308              
309             1;
310              
311             __END__