File Coverage

blib/lib/Test/Mojibake.pm
Criterion Covered Total %
statement 171 176 97.1
branch 93 98 94.9
condition 21 21 100.0
subroutine 14 14 100.0
pod 3 3 100.0
total 302 312 96.7


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   413662 use strict;
  13         31  
  13         362  
6 13     13   65 use warnings qw(all);
  13         23  
  13         640  
7              
8             our $VERSION = '1.1'; # VERSION
9              
10 13     13   10139 use File::Spec::Functions;
  13         10088  
  13         1169  
11 13     13   3084 use Test::Builder;
  13         30908  
  13         1597  
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)
31             eval 'require Unicode::CheckUTF8';
32              
33             ## no critic (ProhibitPackageVars)
34             our $use_xs = $@ ? 0 : 1;
35              
36             sub import {
37 13     13   125 my ($self, @args) = @_;
38 13         31 my $caller = caller;
39              
40 13         36 for my $func (qw(file_encoding_ok all_files all_files_encoding_ok)) {
41             ## no critic (ProhibitNoStrict)
42 13     13   69 no strict 'refs';
  13         22  
  13         18209  
43 39         80 *{$caller."::".$func} = \&$func;
  39         205  
44             }
45              
46 13         81 $Test->exported_to($caller);
47 13         133 $Test->plan(@args);
48              
49 13         5746 return;
50             }
51              
52              
53             ## no critic (ProhibitCascadingIfElse, ProhibitExcessComplexity)
54             sub file_encoding_ok {
55 34     34 1 4393 my ($file, $name) = @_;
56 34 100       137 $name = defined($name) ? $name : "Mojibake test for $file";
57              
58             ## no critic (ProhibitFiletest_f)
59 34 100       640 unless (-f $file) {
60 1         4 $Test->ok(0, $name);
61 1         484 $Test->diag("$file does not exist");
62 1         66 return;
63             }
64              
65 33         50 my $fh;
66 33 50       989 unless (open($fh, '<:raw', $file)) {
67 0         0 close $fh;
68 0         0 $Test->ok(0, $name);
69 0         0 $Test->diag("Can't open $file: $!");
70 0         0 return;
71             }
72              
73 33         60 my $use_utf8 = 0;
74 33         52 my $pod = 0;
75 33         44 my $pod_utf8 = 0;
76 33         49 my $n = 1;
77 33         59 my %pod = ();
78 33         559 while (my $line = <$fh>) {
79 2603 100 100     14997 if (($n == 1) && $line =~ /^\x{EF}\x{BB}\x{BF}/x) {
    100          
    100          
    100          
    100          
80 2         11 $Test->ok(0, $name);
81 2         1399 $Test->diag("UTF-8 BOM (Byte Order Mark) found in $file");
82 2         198 return;
83             } elsif ($line =~ /^=+cut\s*$/x) {
84 11         23 $pod = 0;
85             } elsif ($line =~ /^=+encoding\s+([\w\-]+)/x) {
86 14         48 my $pod_encoding = lc $1;
87 14         46 $pod_encoding =~ y/-//d;
88              
89             # perlpod states:
90             # =encoding affects the whole document, and must occur only once.
91 14         41 ++$pod{$pod_encoding};
92 14 100       48 if (1 < scalar keys %pod) {
93 2         9 $Test->ok(0, $name);
94 2         1115 $Test->diag("POD =encoding redeclared in $file, line $n");
95 2         167 return;
96             }
97              
98 12 100       34 $pod_utf8 = ($pod_encoding eq 'utf8') ? 1 : 0;
99 12         27 $pod = 1;
100             } elsif ($line =~ /^=+\w+/x) {
101 214         387 $pod = 1;
102             } elsif ($pod == 0) {
103             # source
104 1167         1961 $line =~ s/^\s*\#.*$//sx; # disclaimers placed in headers frequently contain UTF-8 *before* its usage is declared.
105 1167         3494 foreach (split m{;}x, $line) {
106             # trim
107 1631         8600 s/^\s+|\s+$//gsx;
108              
109 1631         3492 my @type = qw(0 0 0);
110 1631         2884 ++$type[_detect_utf8(\$_)];
111 1631         2853 my ($latin1, $utf8) = @type[0, 2];
112              
113 1631 100       5929 if (/^use\s+utf8(?:::all)?$/x) {
    100          
    100          
114 6         11 $use_utf8 = 1;
115             } elsif (/^use\s+common::sense$/x) {
116 1         3 $use_utf8 = 1;
117             } elsif (/^no\s+utf8$/x) {
118 2         5 $use_utf8 = 0;
119             }
120              
121 1631 100 100     12182 if (($use_utf8 == 0) && $utf8) {
    100 100        
122 2         9 $Test->ok(0, $name);
123 2         1048 $Test->diag("UTF-8 unexpected in $file, line $n (source)");
124 2         181 return;
125             } elsif (($use_utf8 == 1) && $latin1) {
126 2         12 $Test->ok(0, $name);
127 2         1264 $Test->diag("Non-UTF-8 unexpected in $file, line $n (source)");
128 2         197 return;
129             }
130             }
131             } else {
132             # POD
133 1195         2187 my @type = qw(0 0 0);
134 1195         2148 ++$type[_detect_utf8(\$line)];
135 1195         2132 my ($latin1, $utf8) = @type[0, 2];
136              
137 1195 100 100     7902 if (($pod_utf8 == 0) && $utf8) {
    100 100        
138 2         11 $Test->ok(0, $name);
139 2         1157 $Test->diag("UTF-8 unexpected in $file, line $n (POD)");
140 2         164 return;
141             } elsif (($pod_utf8 == 1) && $latin1) {
142 2         9 $Test->ok(0, $name);
143 2         1038 $Test->diag("Non-UTF-8 unexpected in $file, line $n (POD)");
144 2         163 return;
145             }
146             }
147             } continue {
148 2591         10363 ++$n;
149             }
150 21         183 close $fh;
151              
152 21         103 $Test->ok(1, $name);
153 21         8358 return 1;
154             }
155              
156              
157             sub all_files_encoding_ok {
158 5     5 1 536 my (@args) = @_;
159 5 100       30 @args = _starting_points() unless @args;
160              
161             ## no critic (ProhibitFiletest_f)
162 5 100       17 my @files = map { -d $_ ? all_files($_) : (-f $_ ? $_ : ()) } @args;
  13 100       335  
163              
164 5 100       24 unless (@files) {
165 1         7 $Test->plan(skip_all => 'could not find any files to test');
166 1         6 return;
167             }
168              
169 4         31 $Test->plan(tests => scalar @files);
170              
171 4         854 my $ok = 1;
172 4         12 foreach my $file (@files) {
173 20 100       48 file_encoding_ok($file) or undef $ok;
174             }
175 4         138 return $ok;
176             }
177              
178              
179             sub all_files {
180 3     3 1 16 my (@queue) = @_;
181 3 100       19 @queue = _starting_points() unless @queue;
182 3         7 my @mod = ();
183              
184 3         15 while (@queue) {
185 59         118 my $file = shift @queue;
186 59 100       636 if (-d $file) {
187 29 50       600 opendir my $dh, $file or next;
188 29         318 my @newfiles = readdir $dh;
189 29         193 closedir $dh;
190              
191 29         93 @newfiles = no_upwards(@newfiles);
192 29         372 @newfiles = grep { not exists $ignore_dirs{$_} } @newfiles;
  56         206  
193              
194 29         58 foreach my $newfile (@newfiles) {
195 56         427 my $filename = catfile($file, $newfile);
196 56 100       760 unless (-d $filename) {
197 30         142 push @queue, $filename;
198             } else {
199 26         168 push @queue, catdir($file, $newfile);
200             }
201             }
202             }
203              
204             ## no critic (ProhibitFiletest_f)
205 59 100       657 if (-f $file) {
206 30 100       69 push @mod, $file if _is_perl($file);
207             }
208             }
209 3         24 return @mod;
210             }
211              
212             sub _starting_points {
213 2 50   2   58 return 'blib' if -e 'blib';
214 0         0 return 'lib';
215             }
216              
217             sub _is_perl {
218 30     30   73 my $file = shift;
219              
220 30 100       97 return 1 if $file =~ /\.PL$/x;
221 29 100       139 return 1 if $file =~ /\.p(?:l|m|od)$/x;
222 22 50       53 return 1 if $file =~ /\.t$/x;
223              
224 22 50       597 open my $fh, '<', $file or return;
225 22         232 my $first = <$fh>;
226 22         130 close $fh;
227              
228 22 100 100     139 return 1 if defined $first && ($first =~ /(?:^\#!.*perl)|--\*-Perl-\*--/x);
229              
230 17         112 return;
231             }
232              
233              
234             sub _detect_utf8 {
235              
236 13     13   10493 use bytes;
  13         120  
  13         73  
237 13     13   9764 use integer;
  13         124  
  13         67  
238              
239 59093     59093   1112606 my $str = shift;
240              
241 59093 100       142340 if ($use_xs) {
242 58116 100       71305 if (Unicode::CheckUTF8::is_utf8(${$str})) {
  58116         252996  
243 39674 100       57072 return (${$str} =~ m{[\x{80}-\x{ff}]}x) ? 2 : 1
  39674         175994  
244             } else {
245 18442         46368 return 0;
246             }
247             }
248              
249 977         1364 my $d = 0;
250 977         1376 my $c = 0;
251 977         1272 my $bv = 0;
252 977         1275 my $bits = 0;
253 977         1344 my $len = length ${$str};
  977         2417  
254              
255 977         2992 for (my $i = 0; $i < $len; $i++) {
256 146421         184140 $c = ord(substr(${$str}, $i, 1));
  146421         298890  
257 146421 100       715298 if ($c >= 128) {
    100          
258 4825         6711 $d++;
259              
260 4825 100       29119 if ($c >= 254) {
    100          
    100          
    100          
    100          
    100          
261 1         5 return 0;
262             } elsif ($c >= 252) {
263 1         3 $bits = 6;
264             } elsif ($c >= 248) {
265 1         3 $bits = 5;
266             } elsif ($c >= 240) {
267 1         3 $bits = 4;
268             } elsif ($c >= 224) {
269 322         686 $bits = 3;
270             } elsif ($c >= 192) {
271 4498         6908 $bits = 2;
272             } else {
273 1         11 return 0;
274             }
275              
276 4823 100       11729 if (($i + $bits) > $len) {
277 1         6 return 0;
278             }
279              
280 4822         16923 my @buf = ((0) x 4, $c & ((1 << (6 - $bits)) - 1));
281 4822         12424 while ($bits > 1) {
282 4832         6635 $i++;
283 4832         8203 $bv = ord(substr(${$str}, $i, 1));
  4832         11139  
284 4832 100 100     26834 if (($bv < 128) || ($bv > 191)) {
285 322         2009 return 0;
286             }
287 4510         13196 $buf[7 - $bits] = $bv & 0x3f;
288 4510         14270 $bits--;
289             }
290 4500 100       28662 return 0 if "\0\0\0\0\0\x2f" eq pack 'c6', @buf;
291             } elsif ($c == 0) {
292 2         10 return 0;
293             }
294             }
295              
296 645 100       2904 return $d ? 2 : 1;
297             }
298              
299              
300             1;
301              
302             __END__