File Coverage

blib/lib/Test/Mojibake.pm
Criterion Covered Total %
statement 168 173 97.1
branch 91 96 94.7
condition 21 21 100.0
subroutine 14 14 100.0
pod 3 3 100.0
total 297 307 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 12     12   305153 use strict;
  12         28  
  12         483  
6 12     12   65 use warnings qw(all);
  12         22  
  12         670  
7              
8             our $VERSION = '1.0'; # VERSION
9              
10 12     12   11712 use File::Spec::Functions;
  12         11306  
  12         1271  
11 12     12   3749 use Test::Builder;
  12         38882  
  12         1778  
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 12     12   137 my ($self, @args) = @_;
38 12         34 my $caller = caller;
39              
40 12         37 for my $func (qw(file_encoding_ok all_files all_files_encoding_ok)) {
41             ## no critic (ProhibitNoStrict)
42 12     12   88 no strict 'refs';
  12         23  
  12         19072  
43 36         108 *{$caller."::".$func} = \&$func;
  36         214  
44             }
45              
46 12         95 $Test->exported_to($caller);
47 12         146 $Test->plan(@args);
48              
49 12         5859 return;
50             }
51              
52              
53             ## no critic (ProhibitCascadingIfElse, ProhibitExcessComplexity)
54             sub file_encoding_ok {
55 34     34 1 4772 my ($file, $name) = @_;
56 34 100       147 $name = defined($name) ? $name : "Mojibake test for $file";
57              
58             ## no critic (ProhibitFiletest_f)
59 34 100       800 unless (-f $file) {
60 1         4 $Test->ok(0, $name);
61 1         457 $Test->diag("$file does not exist");
62 1         64 return;
63             }
64              
65 33         53 my $fh;
66 33 50       1353 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         66 my $use_utf8 = 0;
74 33         55 my $pod = 0;
75 33         46 my $pod_utf8 = 0;
76 33         45 my $n = 1;
77 33         73 my %pod = ();
78 33         585 while (my $line = <$fh>) {
79 2549 100 100     13953 if (($n == 1) && $line =~ /^\x{EF}\x{BB}\x{BF}/x) {
    100          
    100          
    100          
    100          
80 2         14 $Test->ok(0, $name);
81 2         1343 $Test->diag("UTF-8 BOM (Byte Order Mark) found in $file");
82 2         208 return;
83             } elsif ($line =~ /^=+cut\s*$/x) {
84 11         20 $pod = 0;
85             } elsif ($line =~ /^=+encoding\s+([\w\-]+)/x) {
86 14         56 my $pod_encoding = lc $1;
87 14         35 $pod_encoding =~ y/-//d;
88              
89             # perlpod states:
90             # =encoding affects the whole document, and must occur only once.
91 14         40 ++$pod{$pod_encoding};
92 14 100       56 if (1 < scalar keys %pod) {
93 2         10 $Test->ok(0, $name);
94 2         1339 $Test->diag("POD =encoding redeclared in $file, line $n");
95 2         214 return;
96             }
97              
98 12 100       39 $pod_utf8 = ($pod_encoding eq 'utf8') ? 1 : 0;
99 12         26 $pod = 1;
100             } elsif ($line =~ /^=+\w+/x) {
101 204         349 $pod = 1;
102             } elsif ($pod == 0) {
103             # source
104 1137         1831 $line =~ s/^\s*\#.*$//sx; # disclaimers placed in headers frequently contain UTF-8 *before* its usage is declared.
105 1137         3256 foreach (split m{;}x, $line) {
106             # trim
107 1593         8835 s/^\s+|\s+$//gsx;
108              
109 1593         3241 my @type = qw(0 0 0);
110 1593         2872 ++$type[_detect_utf8(\$_)];
111 1593         2644 my ($latin1, $utf8) = @type[0, 2];
112              
113 1593 100       5689 if (/^use\s+utf8(?:::all)?$/x) {
    100          
    100          
114 6         11 $use_utf8 = 1;
115             } elsif (/^use\s+common::sense$/x) {
116 1         2 $use_utf8 = 1;
117             } elsif (/^no\s+utf8$/x) {
118 2         5 $use_utf8 = 0;
119             }
120              
121 1593 100 100     11063 if (($use_utf8 == 0) && $utf8) {
    100 100        
122 2         10 $Test->ok(0, $name);
123 2         1009 $Test->diag("UTF-8 unexpected in $file, line $n (source)");
124 2         172 return;
125             } elsif (($use_utf8 == 1) && $latin1) {
126 2         12 $Test->ok(0, $name);
127 2         1279 $Test->diag("Non-UTF-8 unexpected in $file, line $n (source)");
128 2         171 return;
129             }
130             }
131             } else {
132             # POD
133 1181         2093 my @type = qw(0 0 0);
134 1181         1967 ++$type[_detect_utf8(\$line)];
135 1181         2011 my ($latin1, $utf8) = @type[0, 2];
136              
137 1181 100 100     8665 if (($pod_utf8 == 0) && $utf8) {
    100 100        
138 2         11 $Test->ok(0, $name);
139 2         1278 $Test->diag("UTF-8 unexpected in $file, line $n (POD)");
140 2         192 return;
141             } elsif (($pod_utf8 == 1) && $latin1) {
142 2         12 $Test->ok(0, $name);
143 2         1144 $Test->diag("Non-UTF-8 unexpected in $file, line $n (POD)");
144 2         213 return;
145             }
146             }
147             } continue {
148 2537         10235 ++$n;
149             }
150 21         237 close $fh;
151              
152 21         127 $Test->ok(1, $name);
153 21         10989 return 1;
154             }
155              
156              
157             sub all_files_encoding_ok {
158 4     4 1 611 my (@args) = @_;
159 4 100       21 @args = _starting_points() unless @args;
160              
161             ## no critic (ProhibitFiletest_f)
162 4 100       13 my @files = map { -d $_ ? all_files($_) : (-f $_ ? $_ : ()) } @args;
  12 100       366  
163              
164 4         34 $Test->plan(tests => scalar @files);
165              
166 4         1623 my $ok = 1;
167 4         11 foreach my $file (@files) {
168 20 100       60 file_encoding_ok($file) or undef $ok;
169             }
170 4         334 return $ok;
171             }
172              
173              
174             sub all_files {
175 3     3 1 17 my (@queue) = @_;
176 3 100       18 @queue = _starting_points() unless @queue;
177 3         9 my @mod = ();
178              
179 3         19 while (@queue) {
180 59         114 my $file = shift @queue;
181 59 100       712 if (-d $file) {
182 29 50       655 opendir my $dh, $file or next;
183 29         583 my @newfiles = readdir $dh;
184 29         298 closedir $dh;
185              
186 29         97 @newfiles = no_upwards(@newfiles);
187 29         405 @newfiles = grep { not exists $ignore_dirs{$_} } @newfiles;
  56         181  
188              
189 29         56 foreach my $newfile (@newfiles) {
190 56         307 my $filename = catfile($file, $newfile);
191 56 100       828 unless (-d $filename) {
192 30         151 push @queue, $filename;
193             } else {
194 26         182 push @queue, catdir($file, $newfile);
195             }
196             }
197             }
198              
199             ## no critic (ProhibitFiletest_f)
200 59 100       759 if (-f $file) {
201 30 100       68 push @mod, $file if _is_perl($file);
202             }
203             }
204 3         25 return @mod;
205             }
206              
207             sub _starting_points {
208 2 50   2   49 return 'blib' if -e 'blib';
209 0         0 return 'lib';
210             }
211              
212             sub _is_perl {
213 30     30   77 my $file = shift;
214              
215 30 100       92 return 1 if $file =~ /\.PL$/x;
216 29 100       137 return 1 if $file =~ /\.p(?:l|m|od)$/x;
217 22 50       52 return 1 if $file =~ /\.t$/x;
218              
219 22 50       670 open my $fh, '<', $file or return;
220 22         239 my $first = <$fh>;
221 22         204 close $fh;
222              
223 22 100 100     150 return 1 if defined $first && ($first =~ /(?:^\#!.*perl)|--\*-Perl-\*--/x);
224              
225 17         111 return;
226             }
227              
228              
229             sub _detect_utf8 {
230              
231 12     12   11500 use bytes;
  12         122  
  12         67  
232 12     12   11793 use integer;
  12         131  
  12         130  
233              
234 47869     47869   1139164 my $str = shift;
235              
236 47869 100       104123 if ($use_xs) {
237 46892 100       48577 if (Unicode::CheckUTF8::is_utf8(${$str})) {
  46892         224107  
238 32174 100       42021 return (${$str} =~ m{[\x{80}-\x{ff}]}x) ? 2 : 1
  32174         149830  
239             } else {
240 14718         39971 return 0;
241             }
242             }
243              
244 977         1380 my $d = 0;
245 977         1081 my $c = 0;
246 977         1018 my $bv = 0;
247 977         979 my $bits = 0;
248 977         979 my $len = length ${$str};
  977         2659  
249              
250 977         3053 for (my $i = 0; $i < $len; $i++) {
251 146421         163365 $c = ord(substr(${$str}, $i, 1));
  146421         255329  
252 146421 100       652350 if ($c >= 128) {
    100          
253 4825         5826 $d++;
254              
255 4825 100       25064 if ($c >= 254) {
    100          
    100          
    100          
    100          
    100          
256 1         6 return 0;
257             } elsif ($c >= 252) {
258 1         3 $bits = 6;
259             } elsif ($c >= 248) {
260 1         2 $bits = 5;
261             } elsif ($c >= 240) {
262 1         2 $bits = 4;
263             } elsif ($c >= 224) {
264 322         475 $bits = 3;
265             } elsif ($c >= 192) {
266 4498         6670 $bits = 2;
267             } else {
268 1         9 return 0;
269             }
270              
271 4823 100       15322 if (($i + $bits) > $len) {
272 1         7 return 0;
273             }
274              
275 4822         17065 my @buf = ((0) x 4, $c & ((1 << (6 - $bits)) - 1));
276 4822         10393 while ($bits > 1) {
277 4832         10431 $i++;
278 4832         4821 $bv = ord(substr(${$str}, $i, 1));
  4832         9675  
279 4832 100 100     26897 if (($bv < 128) || ($bv > 191)) {
280 322         1870 return 0;
281             }
282 4510         13199 $buf[7 - $bits] = $bv & 0x3f;
283 4510         13777 $bits--;
284             }
285 4500 100       27920 return 0 if "\0\0\0\0\0\x2f" eq pack 'c6', @buf;
286             } elsif ($c == 0) {
287 2         12 return 0;
288             }
289             }
290              
291 645 100       3047 return $d ? 2 : 1;
292             }
293              
294              
295             1;
296              
297             __END__