File Coverage

blib/lib/JSON/Tiny.pm
Criterion Covered Total %
statement 119 120 99.1
branch 74 76 97.3
condition 16 20 80.0
subroutine 25 25 100.0
pod 7 7 100.0
total 241 248 97.1


line stmt bran cond sub pod time code
1             package JSON::Tiny;
2              
3             # Minimalistic JSON. Adapted from Mojo::JSON. (c)2012-2015 David Oswald
4             # License: Artistic 2.0 license.
5             # http://www.perlfoundation.org/artistic_license_2_0
6              
7 4     4   63638 use strict;
  4         5  
  4         97  
8 4     4   11 use warnings;
  4         4  
  4         85  
9 4     4   11 use Carp 'croak';
  4         4  
  4         235  
10 4     4   11 use Exporter 'import';
  4         7  
  4         87  
11 4     4   11 use Scalar::Util 'blessed';
  4         4  
  4         266  
12 4     4   1380 use Encode ();
  4         21346  
  4         5903  
13              
14             our $VERSION = '0.56';
15             our @EXPORT_OK = qw(decode_json encode_json false from_json j to_json true);
16              
17             # Literal names
18             # Users may override Booleans with literal 0 or 1 if desired.
19             our($FALSE, $TRUE) = map { bless \(my $dummy = $_), 'JSON::Tiny::_Bool' } 0, 1;
20              
21             # Escaped special character map with u2028 and u2029
22             my %ESCAPE = (
23             '"' => '"',
24             '\\' => '\\',
25             '/' => '/',
26             'b' => "\x08",
27             'f' => "\x0c",
28             'n' => "\x0a",
29             'r' => "\x0d",
30             't' => "\x09",
31             'u2028' => "\x{2028}",
32             'u2029' => "\x{2029}"
33             );
34             my %REVERSE = map { $ESCAPE{$_} => "\\$_" } keys %ESCAPE;
35              
36             for(0x00 .. 0x1f) {
37             my $packed = pack 'C', $_;
38             $REVERSE{$packed} = sprintf '\u%.4X', $_ unless defined $REVERSE{$packed};
39             }
40              
41             sub decode_json {
42 72     72 1 32516 my $err = _decode(\my $value, shift);
43 72 100       1872 return defined $err ? croak $err : $value;
44             }
45              
46 57     57 1 19794 sub encode_json { Encode::encode 'UTF-8', _encode_value(shift) }
47              
48 8     8 1 375 sub false () {$FALSE} ## no critic (prototypes)
49              
50             sub from_json {
51 3     3 1 364 my $err = _decode(\my $value, shift, 1);
52 3 100       123 return defined $err ? croak $err : $value;
53             }
54              
55             sub j {
56 5 100 66 5 1 1621 return encode_json $_[0] if ref $_[0] eq 'ARRAY' || ref $_[0] eq 'HASH';
57 4         13 return decode_json $_[0];
58             }
59              
60 2     2 1 4 sub to_json { _encode_value(shift) }
61              
62 10     10 1 1862 sub true () {$TRUE} ## no critic (prototypes)
63              
64             sub _decode {
65 75     75   78 my $valueref = shift;
66              
67 75 100       71 eval {
68              
69             # Missing input
70 75 100       190 die "Missing or empty input\n" unless length( local $_ = shift );
71              
72             # UTF-8
73 74 100       108 $_ = eval { Encode::decode('UTF-8', $_, 1) } unless shift;
  71         154  
74 74 100       2966 die "Input is not UTF-8 encoded\n" unless defined $_;
75              
76             # Value
77 72         95 $$valueref = _decode_value();
78              
79             # Leftover data
80 58   66     243 return m/\G[\x20\x09\x0a\x0d]*\z/gc || _throw('Unexpected data');
81             } ? return undef : chomp $@;
82              
83 22         36 return $@;
84             }
85              
86             sub _decode_array {
87 54     54   46 my @array;
88 54         488 until (m/\G[\x20\x09\x0a\x0d]*\]/gc) {
89              
90             # Value
91 75         89 push @array, _decode_value();
92              
93             # Separator
94 70 100       139 redo if m/\G[\x20\x09\x0a\x0d]*,/gc;
95              
96             # End
97 44 100       109 last if m/\G[\x20\x09\x0a\x0d]*\]/gc;
98              
99             # Invalid character
100 3         7 _throw('Expected comma or right square bracket while parsing array');
101             }
102              
103 46         74 return \@array;
104             }
105              
106             sub _decode_object {
107 20     20   15 my %hash;
108 20         70 until (m/\G[\x20\x09\x0a\x0d]*\}/gc) {
109              
110             # Quote
111 28 100       66 m/\G[\x20\x09\x0a\x0d]*"/gc
112             or _throw('Expected string while parsing object');
113              
114             # Key
115 25         26 my $key = _decode_string();
116              
117             # Colon
118 25 100       58 m/\G[\x20\x09\x0a\x0d]*:/gc
119             or _throw('Expected colon while parsing object');
120              
121             # Value
122 24         33 $hash{$key} = _decode_value();
123              
124             # Separator
125 24 100       82 redo if m/\G[\x20\x09\x0a\x0d]*,/gc;
126              
127             # End
128 14 100       38 last if m/\G[\x20\x09\x0a\x0d]*\}/gc;
129              
130             # Invalid character
131 1         4 _throw('Expected comma or right curly bracket while parsing object');
132             }
133              
134 15         25 return \%hash;
135             }
136              
137             sub _decode_string {
138 71     71   92 my $pos = pos;
139            
140             # Extract string with escaped characters
141 71         8402 m!\G((?:(?:[^\x00-\x1f\\"]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})){0,32766})*)!gc; # segfault on 5.8.x in t/20-mojo-json.t
142 71         106 my $str = $1;
143              
144             # Invalid character
145 71 100       129 unless (m/\G"/gc) {
146 2 100       8 _throw('Unexpected character or invalid escape while parsing string')
147             if m/\G[\x00-\x1f\\]/;
148 1         3 _throw('Unterminated string');
149             }
150              
151             # Unescape popular characters
152 69 100       196 if (index($str, '\\u') < 0) {
153 63         246 $str =~ s!\\(["\\/bfnrt])!$ESCAPE{$1}!gs;
154 63         112 return $str;
155             }
156              
157             # Unescape everything else
158 6         8 my $buffer = '';
159 6         25 while ($str =~ m/\G([^\\]*)\\(?:([^u])|u(.{4}))/gc) {
160 13         17 $buffer .= $1;
161              
162             # Popular character
163 13 100       21 if ($2) { $buffer .= $ESCAPE{$2} }
  5         17  
164              
165             # Escaped
166             else {
167 8         21 my $ord = hex $3;
168              
169             # Surrogate pair
170 8 100       19 if (($ord & 0xf800) == 0xd800) {
171              
172             # High surrogate
173 3 100       11 ($ord & 0xfc00) == 0xd800
174             or pos($_) = $pos + pos($str), _throw('Missing high-surrogate');
175              
176             # Low surrogate
177 2 100       12 $str =~ m/\G\\u([Dd][C-Fc-f]..)/gc
178             or pos($_) = $pos + pos($str), _throw('Missing low-surrogate');
179              
180 1         5 $ord = 0x10000 + ($ord - 0xd800) * 0x400 + (hex($1) - 0xdc00);
181             }
182              
183             # Character
184 6         30 $buffer .= pack 'U', $ord;
185             }
186             }
187              
188             # The rest
189 4         21 return $buffer . substr $str, pos $str, length $str;
190             }
191              
192             sub _decode_value {
193              
194             # Leading whitespace
195 171     171   312 m/\G[\x20\x09\x0a\x0d]*/gc;
196              
197             # String
198 171 100       295 return _decode_string() if m/\G"/gc;
199              
200             # Object
201 125 100       193 return _decode_object() if m/\G\{/gc;
202              
203             # Array
204 105 100       193 return _decode_array() if m/\G\[/gc;
205              
206             # Number
207 51         132 my ($i) = /\G([-]?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?)/gc;
208 51 100       137 return 0 + $i if defined $i;
209              
210             # True
211 22 100       52 return $TRUE if m/\Gtrue/gc;
212              
213             # False
214 14 100       38 return $FALSE if m/\Gfalse/gc;
215              
216             # Null
217 7 100       21 return undef if m/\Gnull/gc; ## no critic (return)
218              
219             # Invalid character
220 2         5 _throw('Expected string, array, object, number, boolean or null');
221             }
222              
223             sub _encode_array {
224 44     44   37 '[' . join(',', map { _encode_value($_) } @{$_[0]}) . ']';
  58         62  
  44         82  
225             }
226              
227             sub _encode_object {
228 25     25   24 my $object = shift;
229 25         64 my @pairs = map { _encode_string($_) . ':' . _encode_value($object->{$_}) }
  24         30  
230             sort keys %$object;
231 25         98 return '{' . join(',', @pairs) . '}';
232             }
233              
234             sub _encode_string {
235 55     55   49 my $str = shift;
236 55         490 $str =~ s!([\x00-\x1f\x{2028}\x{2029}\\"/])!$REVERSE{$1}!gs;
237 55         279 return "\"$str\"";
238             }
239              
240             sub _encode_value {
241 143     143   147 my $value = shift;
242              
243             # Reference
244 143 100       239 if (my $ref = ref $value) {
245              
246             # Object
247 88 100       147 return _encode_object($value) if $ref eq 'HASH';
248              
249             # Array
250 63 100       109 return _encode_array($value) if $ref eq 'ARRAY';
251              
252             # True or false
253 19 100       54 return $$value ? 'true' : 'false' if $ref eq 'SCALAR';
    100          
254 11 100       26 return $value ? 'true' : 'false' if $ref eq 'JSON::Tiny::_Bool';
    100          
255              
256             # Blessed reference with TO_JSON method
257 2 50 33     22 if (blessed $value && (my $sub = $value->can('TO_JSON'))) {
258 2         4 return _encode_value($value->$sub);
259             }
260             }
261              
262             # Null
263 55 100       90 return 'null' unless defined $value;
264              
265              
266             # Number (bitwise operators change behavior based on the internal value type)
267              
268             # "0" & $x will modify the flags on the "0" on perl < 5.14, so use a copy
269 50         34 my $zero = "0";
270             # "0" & $num -> 0. "0" & "" -> "". "0" & $string -> a character.
271             # this maintains the internal type but speeds up the xor below.
272 50         75 my $check = $zero & $value;
273 50 100 100     364 return $value
      100        
      100        
274             if length $check
275             # 0 ^ itself -> 0 (false)
276             # $character ^ itself -> "\0" (true)
277             && !($check ^ $check)
278             # filter out "upgraded" strings whose numeric form doesn't strictly match
279             && 0 + $value eq $value
280             # filter out inf and nan
281             && $value * 0 == 0;
282              
283             # String
284 31         43 return _encode_string($value);
285             }
286              
287             sub _throw {
288              
289             # Leading whitespace
290 19     19   27 m/\G[\x20\x09\x0a\x0d]*/gc;
291              
292             # Context
293 19         33 my $context = 'Malformed JSON: ' . shift;
294 19 50       34 if (m/\G\z/gc) { $context .= ' before end of data' }
  0         0  
295             else {
296 19         57 my @lines = split "\n", substr($_, 0, pos);
297 19   100     63 $context .= ' at line ' . @lines . ', offset ' . length(pop @lines || '');
298             }
299              
300 19         118 die "$context\n";
301             }
302              
303             # Emulate boolean type
304             package JSON::Tiny::_Bool;
305 4     4   3964 use overload '""' => sub { ${$_[0]} }, fallback => 1;
  4     40   5549  
  4         30  
  40         2775  
  40         361  
306             1;