File Coverage

blib/lib/JKML.pm
Criterion Covered Total %
statement 137 174 78.7
branch 54 92 58.7
condition 3 8 37.5
subroutine 24 27 88.8
pod 1 8 12.5
total 219 309 70.8


line stmt bran cond sub pod time code
1             package JKML;
2 3     3   56915 use 5.008005;
  3         12  
  3         125  
3 3     3   17 use strict;
  3         7  
  3         111  
4 3     3   28 use warnings;
  3         5  
  3         118  
5 3     3   3080 use parent qw(Exporter);
  3         1140  
  3         19  
6 3     3   3588 use Encode ();
  3         48240  
  3         78  
7 3     3   2990 use MIME::Base64 ();
  3         2539  
  3         77  
8 3     3   3023 use Types::Serialiser;
  3         11967  
  3         86  
9 3     3   23 use B ();
  3         42  
  3         59  
10 3     3   17 use Scalar::Util ();
  3         5  
  3         7886  
11              
12             our @EXPORT = qw(decode_jkml encode_jkml);
13              
14             our $VERSION = "0.01";
15              
16             our @HERE_QUEUE;
17             our $SELF;
18              
19             # JKML is based on JSON::Tiny.
20             # JSON::Tiny was "Adapted from Mojo::JSON and Mojo::Util".
21              
22             # Licensed under the Artistic 2.0 license.
23             # http://www.perlfoundation.org/artistic_license_2_0.
24              
25             sub new {
26 25     25 0 35 my $class = shift;
27 25         142 bless {
28             functions => {
29             base64 => \&MIME::Base64::decode_base64,
30             }
31             }, $class;
32             }
33              
34             sub call {
35 1     1 1 3 my ($self, $name, $vref) = @_;
36 1         11 my $code = $self->{functions}->{$name};
37 1 50       4 unless ($code) {
38 0         0 _exception("Unknown function: $name");
39             }
40 1         15 $code->($vref);
41             }
42              
43             my $TRUE = $Types::Serialiser::true;
44             my $FALSE = $Types::Serialiser::false;
45              
46             # Escaped special character map (with u2028 and u2029)
47             my %ESCAPE = (
48             '"' => '"',
49             '\\' => '\\',
50             '/' => '/',
51             'b' => "\x08",
52             'f' => "\x0c",
53             'n' => "\x0a",
54             'r' => "\x0d",
55             't' => "\x09",
56             'u2028' => "\x{2028}",
57             'u2029' => "\x{2029}"
58             );
59             my %REVERSE = map { $ESCAPE{$_} => "\\$_" } keys %ESCAPE;
60              
61             for( 0x00 .. 0x1f, 0x7f ) {
62             my $packed = pack 'C', $_;
63             $REVERSE{$packed} = sprintf '\u%.4X', $_
64             if ! defined( $REVERSE{$packed} );
65             }
66              
67             my $WHITESPACE_RE = qr/[\x20\x09]/;
68             my $COMMENT_RE = qr!#[^\n]*(?:\n|\z)!;
69             my $IGNORABLE_RE = qr!(?:$WHITESPACE_RE|$COMMENT_RE)*!;
70             my $LEFTOVER_RE = qr!(?:$WHITESPACE_RE|$COMMENT_RE|[\x0d\x0a])*!;
71              
72 17     17 0 97254 sub decode_jkml { JKML->new->decode(shift) }
73 8     8 0 3787 sub encode_jkml { JKML->new->encode(shift) }
74              
75              
76             sub decode {
77 17     17 0 29 my ($self, $bytes) = @_;
78              
79 17         25 local $SELF = $self;
80 17         29 local @HERE_QUEUE;
81              
82             # Missing input
83 17 50       40 die 'Missing or empty input' unless $bytes;
84              
85             # Wide characters
86 17 50       64 die 'Wide character in input'
87             unless utf8::downgrade($bytes, 1);
88              
89             # Object or array
90 17         22 my $res = eval {
91 17         28 local $_ = $bytes;
92              
93             # Leading whitespace
94 17         34 _skip_space();
95              
96             # value
97 17         20 my $ref;
98 17         59 _decode_value(\$ref);
99              
100             # Leftover data
101 17         31 _skip_space();
102 17 50       43 unless (pos() == length($_)) {
103 0 0       0 my $got = ref $ref ? lc(ref($ref)) : 'scalar';
104 0         0 _exception("Unexpected data after $got");
105             }
106              
107 17         45 $ref;
108             };
109              
110             # Exception
111 17 50 33     132 if (!$res && (my $e = $@)) {
112 0         0 chomp $e;
113 0         0 die $e;
114             }
115              
116 17         82 return $res;
117             }
118              
119 0     0 0 0 sub false {$FALSE}
120              
121 0     0 0 0 sub true {$TRUE}
122              
123             sub _decode_array {
124 5     5   7 my @array;
125 5         10 _skip_space();
126 5         16 until (m/\G\]/gc) {
127              
128             # Value
129 8         22 _decode_value(\($array[0+@array]));
130              
131             # Separator
132 8         13 my $found_separator = 0;
133 8         14 _skip_space();
134 8 100       26 if (m/\G,/gc) {
135 4         6 $found_separator++;
136             }
137              
138             # End
139 8         13 _skip_space();
140 8 100       27 last if m/\G\]/gc;
141              
142 3         6 _skip_space();
143 3 50       10 redo if $found_separator;
144              
145             # Invalid character
146 0         0 _exception('Expected comma or right square bracket while parsing array');
147             }
148              
149 5         16 return \@array;
150             }
151              
152             sub _decode_object {
153 5     5   7 my %hash;
154 5         11 _skip_space();
155 5         16 until (m/\G\}/gc) {
156             # Key
157 4         8 my $key = do {
158 4         8 _skip_space();
159 4 50       22 if (m/\G([A-Za-z][a-zA-Z0-9_]*)/gc) {
160 4         15 $1;
161             } else {
162             # Quote
163 0 0       0 m/\G"/gc
164             or _exception('Expected string while parsing object');
165              
166 0         0 _decode_string();
167             }
168             };
169              
170             # Colon
171 4         9 _skip_space();
172 4 50       16 m/\G=>/gc
173             or _exception('Expected "=>" while parsing object');
174              
175             # Value
176 4         16 _decode_value(\$hash{$key});
177              
178             # Separator
179 4         9 _skip_space();
180 4         6 my $found_separator = 0;
181 4 100       12 $found_separator++ if m/\G,/gc;
182              
183             # End
184 4         8 _skip_space();
185 4 50       15 last if m/\G\}/gc;
186              
187 0         0 _skip_space();
188 0 0       0 redo if $found_separator;
189              
190             # Invalid character
191 0         0 _exception('Expected comma or right curly bracket while parsing object');
192             }
193              
194 5         15 return \%hash;
195             }
196              
197             sub _decode_string {
198 3     3   8 my $quote = shift;
199 3         7 my $pos = pos;
200             # Extract string with escaped characters
201 3         146 m!\G((?:(?:[^\x00-\x1f\\${quote}]|\\(?:[${quote}\\/bfnrt]|u[0-9a-fA-F]{4})){0,32766})*)!gc; # segfault on 5.8.x in t/20-mojo-json.t #83
202 3         10 my $str = $1;
203              
204             # Invalid character
205 3 50       33 unless (m/\G${quote}/gc) {
206 0 0       0 _exception('Unexpected character or invalid escape while parsing string')
207             if m/\G[\x00-\x1f\\]/;
208 0         0 _exception('Unterminated string');
209             }
210              
211             # Unescape popular characters
212 3 50       13 if (index($str, '\\u') < 0) {
213 3         7 $str =~ s!\\(["\\/bfnrt])!$ESCAPE{$1}!gs;
214 3         12 return $str;
215             }
216              
217             # Unescape everything else
218 0         0 my $buffer = '';
219 0         0 while ($str =~ m/\G([^\\]*)\\(?:([^u])|u(.{4}))/gc) {
220 0         0 $buffer .= $1;
221              
222             # Popular character
223 0 0       0 if ($2) { $buffer .= $ESCAPE{$2} }
  0         0  
224              
225             # Escaped
226             else {
227 0         0 my $ord = hex $3;
228              
229             # Surrogate pair
230 0 0       0 if (($ord & 0xf800) == 0xd800) {
231              
232             # High surrogate
233 0 0       0 ($ord & 0xfc00) == 0xd800
234             or pos($_) = $pos + pos($str), _exception('Missing high-surrogate');
235              
236             # Low surrogate
237 0 0       0 $str =~ m/\G\\u([Dd][C-Fc-f]..)/gc
238             or pos($_) = $pos + pos($str), _exception('Missing low-surrogate');
239              
240             # Pair
241 0         0 $ord = 0x10000 + ($ord - 0xd800) * 0x400 + (hex($1) - 0xdc00);
242             }
243              
244             # Character
245 0         0 $buffer .= pack 'U', $ord;
246             }
247             }
248              
249             # The rest
250 0         0 return $buffer . substr $str, pos($str), length($str);
251             }
252              
253             sub _skip_space {
254 109     109   711 while (m/\G$IGNORABLE_RE/gc) {
255 121 100       690 if (m/\G\x0d?\x0a/gc) {
256 33 100       168 if (@HERE_QUEUE) {
257 6         7 my ($v, $terminator) = @{pop @HERE_QUEUE};
  6         13  
258 6 50       122 if (m/\G(.*?)^([ \t]*)$terminator$/gcsm) {
259 6         13 my $buf = $1;
260 6         130 my $dedent = $2;
261 6         57 $buf =~ s/^$dedent//mg;
262 6         20 $buf =~ s/\n\z//;
263 6         70 $$v = $buf;
264             } else {
265 0         0 _exception("Unexpected EOF in heredoc: '$terminator'");
266             }
267             }
268             }
269             }
270             }
271              
272             sub _decode_value {
273 30     30   44 my $r = shift;
274              
275             # Leading whitespace
276 30         46 _skip_space();
277              
278             # funcall
279 30 100       77 if (m/\G([a-zA-Z][a-zA-Z0-9_]*)\(/gc) {
280 1         3 my $func = $1;
281 1         4 _decode_value(\my $v);
282 1 50       5 m/\G\)/gc or _exception("Missing ) after funcall");
283 1         5 return $$r = $SELF->call($func, $v);
284             }
285              
286             # heredoc
287 29 100       79 if (m/\G<<-([A-Za-z.]+)/gc) {
288 6         20 push @HERE_QUEUE, [$r, $1];
289 6         9 return;
290             }
291              
292             # Raw string
293 23 100       136 return $$r = $2 if m/\Gr('''|""""|'|")(.*?)\1/gc;
294              
295             # String
296 17 100       49 return $$r = _decode_string($1) if m/\G(["'])/gc;
297              
298             # Array
299 14 100       47 return $$r = _decode_array() if m/\G\[/gc;
300              
301             # Object
302 9 100       33 return $$r = _decode_object() if m/\G\{/gc;
303              
304             # Number
305 4 100       21 return $$r = 0 + $1
306             if m/\G([-]?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?)/gc;
307              
308             # True
309 3 100       15 return $$r = $TRUE if m/\Gtrue/gc;
310              
311             # False
312 1 50       3 return $$r = $FALSE if m/\Gfalse/gc;
313              
314             # Null
315 1 50       7 return $$r = undef if m/\Gnull/gc; ## no critic (return)
316              
317             # Invalid character
318 0         0 _exception('Expected string, array, object, number, boolean or null');
319             }
320              
321             sub _exception {
322              
323             # Leading whitespace
324 0     0   0 _skip_space();
325              
326             # Context
327 0         0 my $context = 'Malformed JKML: ' . shift;
328 0 0       0 if (m/\G\z/gc) { $context .= ' before end of data' }
  0         0  
329             else {
330 0         0 my @lines = split /\n/, substr($_, 0, pos);
331 0   0     0 $context .= ' at line ' . @lines . ', offset ' . length(pop @lines || '');
332             }
333              
334 0         0 die "$context\n";
335             }
336              
337             # -------------------------------------------------------------------------
338              
339             sub encode {
340 8     8 0 12 my ($self, $ref) = @_;
341 8         18 return Encode::encode 'UTF-8', _encode_value($ref);
342             }
343              
344             sub _encode_array {
345 1     1   2 my $array = shift;
346 1         10 return '[' . join(',', map { _encode_value($_) } @$array) . ']';
  0         0  
347             }
348              
349             sub _encode_object {
350 2     2   3 my $object = shift;
351 2         5 my @pairs = map { _encode_string($_) . '=>' . _encode_value($object->{$_}) }
  1         19  
352             keys %$object;
353 2         10 return '{' . join(',', @pairs) . '}';
354             }
355              
356             sub _encode_string {
357 3     3   4 my $str = shift;
358 3         12 $str =~ s!([\x00-\x1f\x7f\x{2028}\x{2029}\\"/\b\f\n\r\t])!$REVERSE{$1}!gs;
359 3         12 return "\"$str\"";
360             }
361              
362             sub _encode_value {
363 9     9   11 my $value = shift;
364              
365             # Reference
366 9 100       19 if (my $ref = ref $value) {
367              
368             # Array
369 5 100       15 return _encode_array($value) if $ref eq 'ARRAY';
370              
371             # Object
372 4 100       12 return _encode_object($value) if $ref eq 'HASH';
373              
374             # True or false
375 2 0       7 return Types::Serialiser::is_true($value) ? 'true' : 'false' if Types::Serialiser::is_bool($ref);
    50          
376              
377             # References to scalars (including blessed) will be encoded as Booleans.
378 2 100       26 return $$value ? 'true' : 'false' if $ref =~ /SCALAR/;
    50          
379              
380             }
381              
382             # Null
383 4 50       8 return 'null' unless defined $value;
384              
385             # Number
386 4         39 my $flags = B::svref_2object(\$value)->FLAGS;
387 4 100 66     33 return 0 + $value if $flags & (B::SVp_IOK | B::SVp_NOK) && $value * 0 == 0;
388              
389             # String
390 2         6 return _encode_string($value);
391             }
392              
393             1;
394             __END__