File Coverage

lib/XML/Compile/Schema/BuiltInFacets.pm
Criterion Covered Total %
statement 145 195 74.3
branch 57 104 54.8
condition 10 36 27.7
subroutine 49 66 74.2
pod 1 1 100.0
total 262 402 65.1


line stmt bran cond sub pod time code
1             # Copyrights 2006-2019 by [Mark Overmeer ].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution XML-Compile. Meta-POD processed with
6             # OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package XML::Compile::Schema::BuiltInFacets;
10 50     50   295 use vars '$VERSION';
  50         89  
  50         2230  
11             $VERSION = '1.63';
12              
13 50     50   269 use base 'Exporter';
  50         84  
  50         5628  
14              
15 50     50   257 use warnings;
  50         76  
  50         909  
16 50     50   190 use strict;
  50         98  
  50         1302  
17 50     50   258 no warnings 'recursion';
  50         135  
  50         2972  
18              
19             our @EXPORT = qw/builtin_facet/;
20              
21 50     50   344 use Log::Report 'xml-compile';
  50         98  
  50         264  
22              
23 50     50   10208 use Math::BigInt;
  50         104  
  50         353  
24 50     50   15434 use Math::BigFloat;
  50         88  
  50         225  
25 50     50   24713 use XML::LibXML; # for ::RegExp
  50         119  
  50         350  
26 50     50   7265 use XML::Compile::Util qw/SCHEMA2001 pack_type duration2secs/;
  50         84  
  50         2206  
27              
28 50     50   258 use POSIX qw/DBL_MAX_10_EXP DBL_DIG/;
  50         80  
  50         402  
29              
30             # depend on Perl's compile flags
31 50     50   4827 use constant INT_MAX => int((sprintf"%u\n",-1)/2);
  50         110  
  50         3964  
32 50     50   276 use constant INT_MIN => -1 - INT_MAX;
  50         86  
  50         113927  
33              
34              
35             my %facets_simple =
36             ( enumeration => \&_enumeration
37             , fractionDigits => \&_s_fractionDigits
38             , length => \&_s_length
39             , maxExclusive => \&_s_maxExclusive
40             , maxInclusive => \&_s_maxInclusive
41             , maxLength => \&_s_maxLength
42             , maxScale => undef # ignore
43             , minExclusive => \&_s_minExclusive
44             , minInclusive => \&_s_minInclusive
45             , minLength => \&_s_minLength
46             , minScale => undef # ignore
47             , pattern => \&_pattern
48             , totalDigits => \&_s_totalDigits
49             , whiteSpace => \&_s_whiteSpace
50             , _totalFracDigits=> \&_s_totalFracDigits
51             );
52              
53             my %facets_list =
54             ( enumeration => \&_enumeration
55             , length => \&_list_length
56             , maxLength => \&_list_maxLength
57             , minLength => \&_list_minLength
58             , pattern => \&_pattern
59             , whiteSpace => \&_list_whiteSpace
60             );
61              
62             my %facets_date = # inclusive or exclusive times is rather useless.
63             ( enumeration => \&_enumeration
64             , explicitTimeZone=> \&_date_expl_tz
65             , maxExclusive => \&_date_max
66             , maxInclusive => \&_date_max
67             , minExclusive => \&_date_min
68             , minInclusive => \&_date_min
69             , pattern => \&_pattern
70             , whiteSpace => \&_date_whiteSpace
71             );
72              
73             my %facets_duration =
74             ( enumeration => \&_enumeration
75             , maxExclusive => \&_dur_max_excl
76             , maxInclusive => \&_dur_max_incl
77             , minExclusive => \&_dur_min_excl
78             , minInclusive => \&_dur_min_incl
79             , pattern => \&_pattern
80             , whiteSpace => \&_s_whiteSpace
81             );
82              
83             my $date_time_type = pack_type SCHEMA2001, 'dateTime';
84             my $date_type = pack_type SCHEMA2001, 'date';
85             my $duration_type = pack_type SCHEMA2001, 'duration';
86              
87             sub builtin_facet($$$$$$$$)
88 243     243 1 559 { my ($path, $args, $facet, $value, $is_list, $type, $nss, $action) = @_;
89              
90             my $def
91             = $is_list ? $facets_list{$facet}
92             : $nss->doesExtend($type, $date_time_type) ? $facets_date{$facet}
93             : $nss->doesExtend($type, $date_type) ? $facets_date{$facet}
94             : $nss->doesExtend($type, $duration_type) ? $facets_duration{$facet}
95 243 100       691 : $facets_simple{$facet};
    50          
    100          
    100          
96              
97 243 50       685 $def or error __x"facet {facet} not implemented at {where}"
98             , facet => $facet, where => $path;
99              
100 243         492 $def->($path, $args, $value, $type, $nss, $action);
101             }
102              
103             sub _list_whiteSpace($$$)
104 0     0   0 { my ($path, undef, $ws) = @_;
105 0 0       0 $ws eq 'collapse'
106             or error __x"list whiteSpace facet fixed to 'collapse', not '{ws}' in {path}"
107             , ws => $ws, path => $path;
108 0         0 ();
109             }
110              
111             sub _s_whiteSpace($$$)
112 6     6   12 { my ($path, undef, $ws) = @_;
113 6 50       29 $ws eq 'replace' ? \&_whitespace_replace
    100          
    100          
114             : $ws eq 'collapse' ? \&_whitespace_collapse
115             : $ws eq 'preserve' ? ()
116             : error __x"illegal whiteSpace facet '{ws}' in {path}"
117             , ws => $ws, path => $path;
118             }
119              
120             sub _date_whiteSpace($$$)
121 0     0   0 { my ($path, undef, $ws) = @_;
122              
123             # whitespace processing already in the dateTime parser
124 0 0       0 $ws eq 'collapse'
125             or error __x"illegal whiteSpace facet '{ws}' in {path}"
126             , ws => $ws, path => $path;
127 0         0 ();
128             }
129              
130             sub _whitespace_replace($)
131 2 100   2   8 { my $value = ref $_[0] ? $_[0]->textContent : $_[0];
132 2         13 $value =~ s/[\t\r\n]/ /gs;
133 2         6 $value;
134             }
135              
136             sub _whitespace_collapse($)
137 2 100   2   10 { my $value = ref $_[0] ? $_[0]->textContent : $_[0];
138 2         5 for($value)
139 2         12 { s/[\t\r\n ]+/ /gs;
140 2         9 s/^ +//;
141 2         9 s/ +$//;
142             }
143 2         6 $value;
144             }
145              
146             sub _maybe_big($$$)
147 72     72   99 { my ($path, $args, $value) = @_;
148 72 100       134 return $value if $args->{sloppy_integers};
149              
150             # modules Math::Big* loaded by Schema::Spec when not sloppy
151              
152 70         141 $value =~ s/\s//g;
153 70 50       216 if($value =~ m/[.eE]/)
    50          
154 0         0 { my $c = $value;
155 0 0       0 my $exp = $c =~ s/[eE][+-]?([0-9]+)// ? $1 : 0;
156 0 0       0 my $pre = $c =~ /^[-+]?([0-9]*)/ ? length($1) : 0;
157 0 0 0     0 return Math::BigFloat->new($value)
158             if $pre >= DBL_DIG || $pre+$exp >= DBL_MAX_10_EXP;
159             }
160             # compare ints as strings, because they will overflow!!
161             elsif(substr($value, 0, 1) eq '-')
162 0 0 0     0 { return Math::BigInt->new($value)
      0        
163             if length($value) > length(INT_MIN)
164             || (length($value)==length(INT_MIN) && $value gt INT_MIN);
165             }
166             else
167 70 100 33     240 { return Math::BigInt->new($value)
      66        
168             if length($value) > length(INT_MAX)
169             || (length($value)==length(INT_MAX) && $value gt INT_MAX);
170             }
171              
172 64         135 $value;
173             }
174              
175             sub _s_minInclusive($$$)
176 33     33   78 { my ($path, $args, $min) = @_;
177 33         68 $min = _maybe_big $path, $args, $min;
178 24 100   24   198 sub { return $_[0] if $_[0] >= $min;
179 7         23 error __x"too small inclusive {value}, min {min} at {where}"
180             , value => $_[0], min => $min, where => $path;
181 33         171 };
182             }
183              
184             sub _s_minExclusive($$$)
185 6     6   12 { my ($path, $args, $min) = @_;
186 6         11 $min = _maybe_big $path, $args, $min;
187 5 100   5   18 sub { return $_[0] if $_[0] > $min;
188 2         8 error __x"too small exclusive {value}, larger {min} at {where}"
189             , value => $_[0], min => $min, where => $path;
190 6         27 };
191             }
192              
193             sub _s_maxInclusive($$$)
194 21     21   46 { my ($path, $args, $max) = @_;
195 21         40 $max = _maybe_big $path, $args, $max;
196 19 100   19   301 sub { return $_[0] if $_[0] <= $max;
197 6         92 error __x"too large inclusive {value}, max {max} at {where}"
198             , value => $_[0], max => $max, where => $path;
199 21         406 };
200             }
201              
202             sub _s_maxExclusive($$$)
203 12     12   24 { my ($path, $args, $max) = @_;
204 12         22 $max = _maybe_big $path, $args, $max;
205 5 100   5   15 sub { return $_[0] if $_[0] < $max;
206 2         6 error __x"too large exclusive {value}, smaller {max} at {where}"
207             , value => $_[0], max => $max, where => $path;
208 12         51 };
209             }
210              
211             my $qname = pack_type SCHEMA2001, 'QName';
212             sub _enumeration($$$$$$)
213 62     62   114 { my ($path, $args, $enums, $type, $nss, $action) = @_;
214              
215 62         255 my %enum = map +($_ => 1), @$enums;
216 48 100   48   111 sub { my $v = ref $_[0] eq 'ARRAY' ? join(' ', @{$_[0]}) : $_[0];
  9         30  
217 48 100       168 return $v if exists $enum{$v};
218 18         57 error __x"invalid enumerate `{string}' at {where}"
219             , string => $v, where => $path;
220 62         345 };
221             }
222              
223             sub _s_totalDigits($$$)
224 26     26   51 { my ($path, undef, $total) = @_;
225              
226             # this accidentally also works correctly for NaN +INF -INF
227             sub
228 26     26   64 { my $v = $_[0];
229 26         71 $v =~ s/[eE].*//;
230 26         91 $v =~ s/^[+-]?0*//;
231 26 100       111 return $_[0] if $total >= ($v =~ tr/0-9//);
232              
233 2         9 error __x"decimal too long, got {length} digits max {max} at {where}"
234             , length => ($v =~ tr/0-9//), max => $total, where => $path;
235 26         111 };
236             }
237              
238             sub _s_fractionDigits($$$)
239 2     2   3 { my $frac = $_[2];
240             # can be result from Math::BigFloat, so too long to use %f But rounding
241             # is very hard to implement. If you need this accuracy, then format your
242             # value yourself!
243             sub
244 4     4   6 { my $v = $_[0];
245 4         53 $v =~ s/(\.[0-9]{$frac}).*/$1/;
246 4         259 $v;
247 2         11 };
248             }
249              
250             sub _s_totalFracDigits($$$)
251 3     3   6 { my ($path, undef, $dig) = @_;
252 3         7 my ($total, $frac) = @$dig;
253             sub
254 4     4   6 { my $w = $_[0];
255              
256 4         5 my $v = $w; # total is checking length
257 4         35 $v =~ s/[eE].*//;
258 4         151 $v =~ s/^[+-]?0*//;
259              
260 4 50       240 if( $v !~ /^(?:[+-]?)(?:[0-9]*)(?:\.([0-9]*))?$/ )
261 0         0 { error __x"Invalid numeric format, got {value} at {where}"
262             , value => $w, where => $path;
263             }
264              
265 4 100 66     21 if($1 && length($1) > $frac)
266 1         6 { error __x"fractional part for {value} too long, got {l} digits max {max} at {where}"
267             , value => $w, l => length($1), max => $frac, where => $path;
268             }
269              
270 3 50       14 return $w if $total >= ($v =~ tr/0-9//);
271 0         0 error __x"decimal too long, got {length} digits max {max} at {where}"
272             , length => ($v =~ tr/0-9//), max => $total, where => $path;
273 3         17 };
274             }
275              
276             sub _s_length($$$$$$)
277 19     19   42 { my ($path, $args, $len, $type, $nss, $action) = @_;
278              
279 15 100 66 15   76 sub { return $_[0] if defined $_[0] && length($_[0])==$len;
280 9         30 error __x"string `{string}' does not have required length {len} but {size} at {where}"
281             , string => $_[0], len => $len, size => length($_[0]), where => $path;
282 19         93 };
283             }
284              
285             sub _list_length($$$)
286 6     6   10 { my ($path, $args, $len) = @_;
287 6 100 66 6   13 sub { return $_[0] if defined $_[0] && @{$_[0]}==$len;
  6         21  
288 4         14 error __x"list `{list}' does not have required length {len} at {where}"
289             , list => $_[0], len => $len, where => $path;
290 6         36 };
291             }
292              
293             sub _s_minLength($$$)
294 8     8   26 { my ($path, $args, $len, $type, $nss, $action) = @_;
295              
296 4 50 33 4   29 sub { return $_[0] if defined $_[0] && length($_[0]) >=$len;
297 0         0 error __x"string `{string}' does not have minimum length {len} at {where}"
298             , string => $_[0], len => $len, where => $path;
299 8         44 };
300             }
301              
302             sub _list_minLength($$$)
303 0     0   0 { my ($path, $args, $len) = @_;
304 0 0 0 0   0 sub { return $_[0] if defined $_[0] && @{$_[0]} >=$len;
  0         0  
305 0         0 error __x"list `{list}' does not have minimum length {len} at {where}"
306             , list => $_[0], len => $len, where => $path;
307 0         0 };
308             }
309              
310             sub _s_maxLength($$$)
311 0     0   0 { my ($path, $args, $len, $type, $nss, $action) = @_;
312              
313 0 0 0 0   0 sub { return $_[0] if defined $_[0] && length $_[0] <= $len;
314 0         0 error __x"string `{string}' longer than maximum length {len} at {where}"
315             , string => $_[0], len => $len, where => $path;
316 0         0 };
317             }
318              
319             sub _list_maxLength($$$)
320 0     0   0 { my ($path, $args, $len) = @_;
321 0 0 0 0   0 sub { return $_[0] if defined $_[0] && @{$_[0]} <= $len;
  0         0  
322 0         0 error __x"list `{list}' longer than maximum length {len} at {where}"
323             , list => $_[0], len => $len, where => $path;
324 0         0 };
325             }
326              
327             sub _pattern($$$)
328 30     30   61 { my ($path, $args, $pats) = @_;
329 30 50       64 @$pats or return ();
330 30 50       64 my $regex = @$pats==1 ? $pats->[0] : "(".join(')|(', @$pats).")";
331 30         112 my $compiled = XML::LibXML::RegExp->new($regex);
332              
333             sub {
334 50     50   409 use Carp 'cluck';
  50         112  
  50         38116  
335 26 50   26   55 defined $_[0] or cluck "PATTERN";
336 26 100       76 my $v = ref $_[0] ? $_[0]->textContent : $_[0];
337 26 100       209 return $_[0] if $compiled->matches($v);
338 6         28 error __x"string `{string}' does not match pattern `{pat}' at {where}"
339             , string => $v, pat => $regex, where => $path;
340 30         844 };
341             }
342              
343             sub _date_min($$$)
344 2     2   5 { my ($path, $args, $min) = @_;
345 2 50   2   9 sub { return $_[0] if $_[0] gt $min;
346 0         0 error __x"too small inclusive {value}, min {min} at {where}"
347             , value => $_[0], min => $min, where => $path;
348 2         9 };
349             }
350              
351             sub _date_max($$$)
352 2     2   5 { my ($path, $args, $max) = @_;
353 2 50   2   8 sub { return $_[0] if $_[0] lt $max;
354 0         0 error __x"too large inclusive {value}, max {max} at {where}"
355             , value => $_[0], max => $max, where => $path;
356 2         18 };
357             }
358              
359             sub _date_expl_tz($$$)
360 0     0   0 { my ($path, $args, $enum) = @_;
361 0         0 my $tz = qr/Z$ | [+-](?:(?:0[0-9]|1[0-3])\:[0-5][0-9] | 14\:00)$/x;
362              
363             $enum eq 'optional' ? ()
364             : $enum eq 'prohibited'
365 0 0   0   0 ? sub { $_[0] !~ $tz
366             or error __x"timezone forbidden on {date} at {where}"
367             , date => $_[0], where => $path;
368             }
369             : $enum eq 'required'
370 0 0   0   0 ? sub { $_[0] =~ $tz
371             or error __x"timezone required on {date} at {where}"
372             , date => $_[0], where => $path;
373             }
374 0 0       0 : error __x"illegal explicitTimeZone facet '{enum}' in {path}"
    0          
    0          
375             , enum => $enum, path => $path;
376             }
377              
378             sub _dur_min_incl($$$)
379 5     5   8 { my ($path, $args, $min) = @_;
380 5         20 my $secs = duration2secs $min;
381              
382 5 100   5   13 sub { return $_[0] if duration2secs $_[0] >= $secs;
383 1         5 error __x"too small minInclusive duration {value}, min {min} at {where}"
384             , value => $_[0], min => $min, where => $path;
385 5         37 };
386             }
387              
388              
389             sub _dur_min_excl($$$)
390 0     0     { my ($path, $args, $min) = @_;
391 0           my $secs = duration2secs $min;
392              
393 0 0   0     sub { return $_[0] if duration2secs $_[0] > $secs;
394 0           error __x"too small minExclusive duration {value}, min {min} at {where}"
395             , value => $_[0], min => $min, where => $path;
396 0           };
397             }
398              
399              
400             sub _dur_max_incl($$$)
401 0     0     { my ($path, $args, $max) = @_;
402 0           my $secs = duration2secs $max;
403              
404 0 0   0     sub { return $_[0] if duration2secs $_[0] <= $secs;
405 0           error __x"too large maxInclusive duration {value}, max {max} at {where}"
406             , value => $_[0], max => $max, where => $path;
407 0           };
408             }
409              
410              
411             sub _dur_max_excl($$$)
412 0     0     { my ($path, $args, $max) = @_;
413 0           my $secs = duration2secs $max;
414              
415 0 0   0     sub { return $_[0] if duration2secs $_[0] < $secs;
416 0           error __x"too large maxExclusive duration {value}, max {max} at {where}"
417             , value => $_[0], max => $max, where => $path;
418 0           };
419             }
420              
421             1;