File Coverage

blib/lib/TOML/Tiny/Parser.pm
Criterion Covered Total %
statement 292 304 96.0
branch 107 116 92.2
condition 27 35 77.1
subroutine 37 38 97.3
pod 0 21 0.0
total 463 514 90.0


line stmt bran cond sub pod time code
1             package TOML::Tiny::Parser;
2             # ABSTRACT: parser used by TOML::Tiny
3             $TOML::Tiny::Parser::VERSION = '0.16';
4 286     286   239670 use utf8;
  286         609  
  286         1988  
5 286     286   8888 use strict;
  286         728  
  286         5412  
6 286     286   1353 use warnings;
  286         603  
  286         9145  
7 286     286   1793 no warnings qw(experimental);
  286         700  
  286         9532  
8 286     286   3433 use v5.18;
  286         1200  
9              
10 286     286   1889 use Carp qw(confess);
  286         677  
  286         16718  
11 286     286   122771 use Data::Dumper qw(Dumper);
  286         1208981  
  286         18715  
12 286     286   2290 use Encode qw(decode FB_CROAK);
  286         648  
  286         15032  
13 286     286   225767 use Math::BigFloat ();
  286         10395801  
  286         9065  
14 286     286   2684 use Math::BigInt ();
  286         696  
  286         6959  
15 286     286   154245 use TOML::Tiny::Grammar qw($TimeOffset);
  286         989  
  286         47906  
16 286     286   128494 use TOML::Tiny::Tokenizer ();
  286         934  
  286         863636  
17              
18             our $TRUE = 1;
19             our $FALSE = 0;
20              
21             eval{
22             require Types::Serialiser;
23             $TRUE = Types::Serialiser::true();
24             $FALSE = Types::Serialiser::false();
25             };
26              
27             sub new {
28 430     430 0 605874 my ($class, %param) = @_;
29             bless{
30             inflate_integer => $param{inflate_integer},
31             inflate_float => $param{inflate_float},
32 53     53   246 inflate_datetime => $param{inflate_datetime} || sub{ shift },
33 35 100   35   218 inflate_boolean => $param{inflate_boolean} || sub{ shift eq 'true' ? $TRUE : $FALSE },
34             strict => $param{strict},
35 430   100     9695 }, $class;
      100        
36             }
37              
38             sub next_token {
39 6358     6358 0 9718 my $self = shift;
40 6358   66     22191 my $token = $self->{tokenizer} && $self->{tokenizer}->next_token;
41 6244         18627 return $token;
42             }
43              
44             sub parse {
45 430     430 0 1473 my ($self, $toml) = @_;
46              
47 430 100       3927 if ($self->{strict}) {
48 185         2164 $toml = decode('UTF-8', "$toml", FB_CROAK);
49             }
50              
51 424         17529 $self->{tokenizer} = TOML::Tiny::Tokenizer->new(source => $toml);
52 424         1536 $self->{keys} = [];
53 424         1665 $self->{root} = {};
54 424         1215 $self->{tables} = {}; # "seen" hash of explicitly defined table names (e.g. [foo])
55 424         1199 $self->{arrays} = {}; # "seen" hash of explicitly defined static arrays (e.g. foo=[])
56 424         1081 $self->{array_tables} = {}; # "seen" hash of explicitly defined arrays of tables (e.g. [[foo]])
57              
58 424         1871 $self->parse_table;
59 245         737 my $result = $self->{root};
60              
61 245         1592 delete $self->{tokenizer};
62 245         737 delete $self->{keys};
63 245         583 delete $self->{root};
64 245         695 delete $self->{tables};
65 245         603 delete $self->{arrays};
66 245         460 delete $self->{array_tables};
67              
68 245         849 return $result;
69             }
70              
71             sub parse_error {
72 60     60 0 286 my ($self, $token, $msg) = @_;
73 60 100       332 my $line = $token ? $token->{line} : 'EOF';
74 60 50       329 if ($ENV{TOML_TINY_DEBUG}) {
75 0         0 my $root = Dumper($self->{root});
76 0         0 my $tok = Dumper($token);
77 0         0 my $src = substr $self->{tokenizer}{source}, $self->{tokenizer}{position}, 30;
78              
79 0         0 confess qq{
80             toml parse error at line $line:
81             $msg
82              
83             Current token:
84             $tok
85              
86             Parse state:
87             $root
88              
89             Source near location of error:
90             ...
91             $src
92             ...
93              
94             };
95             } else {
96 60         887 die "toml parse error at line $line: $msg\n";
97             }
98             }
99              
100             sub expect_type {
101 3551     3551 0 7854 my ($self, $token, $expected) = @_;
102 3551 100       7678 my $actual = $token ? $token->{type} : 'EOF';
103 3551 100       64802 $self->parse_error($token, "expected $expected, but found $actual")
104             unless $actual =~ /$expected/;
105             }
106              
107              
108             sub current_key {
109 891     891 0 1483 my $self = shift;
110 891         1938 my @keys = $self->get_keys;
111 891         2587 my $key = join '.', map{ qq{"$_"} } @keys;
  735         2640  
112 891         3130 return $key;
113             }
114              
115             sub push_keys {
116 1710     1710 0 3452 my ($self, $token) = @_;
117 1710         2590 push @{ $self->{keys} }, $token->{value};
  1710         4591  
118             }
119              
120             sub pop_keys {
121 1220     1220 0 1965 my $self = shift;
122 1220         1732 pop @{ $self->{keys} };
  1220         2523  
123             }
124              
125             sub get_keys {
126 2564     2564 0 3772 my $self = shift;
127 2564         3681 return map{ @$_ } @{ $self->{keys} };
  3664         10526  
  2564         5455  
128             }
129              
130             sub set_key {
131 936     936 0 2464 my ($self, $token) = @_;
132 936         2222 my @keys = $self->get_keys;
133 936         2053 my $key = pop @keys;
134 936         2645 my $node = $self->scan_to_key(\@keys);
135              
136 935 100 100     5172 if ($key && exists $node->{$key}) {
137 2         9 $self->parse_error($token, 'duplicate key: ' . $self->current_key);
138             }
139              
140 933         2832 $node->{$key} = $self->parse_value($token);
141             }
142              
143             sub declare_key {
144 889     889 0 1945 my ($self, $token) = @_;
145 889   100     2285 my $key = $self->current_key || return;
146              
147 467 100       1418 if ($token->{type} eq 'inline_array') {
148             $self->parse_error($token, "duplicate key: $key")
149 162 50       557 if exists $self->{array_tables}{$key};
150              
151 162         546 $self->{arrays}{$key} = 1;
152 162         345 return;
153             }
154              
155 305 100       846 if ($token->{type} eq 'array_table') {
156 74 100       251 if (exists $self->{arrays}{$key}) {
157 1         5 $self->parse_error($token, "duplicate key: $key");
158             }
159              
160 73         199 $self->{array_tables}{$key} = 1;
161 73         149 return;
162             }
163              
164 231 50       650 if ($token->{type} eq 'table') {
165             $self->parse_error($token, "duplicate key: $key")
166             if exists $self->{arrays}{$key}
167 231 100 66     1201 || exists $self->{array_tables}{$key};
168              
169 229 100       614 if (exists $self->{tables}{$key}) {
170             # Tables cannot be redefined, *except* when doing so within a goddamn
171             # table array. Gawd I hate TOML.
172 9         27 my $in_a_stupid_table_array = 0;
173 9         21 my $node = $self->{root};
174              
175 9         20 for my $key ($self->get_keys) {
176 9 100 66     108 if (exists $node->{$key} && ref($node->{$key}) eq 'ARRAY') {
177 8         17 $in_a_stupid_table_array = 1;
178 8         14 last;
179             } else {
180 1         3 $node = $node->{$key};
181             }
182             }
183              
184 9 100       27 unless ($in_a_stupid_table_array) {
185 1         6 $self->parse_error($token, "duplicate key: $key");
186             }
187 8         21 return;
188             }
189 220         758 $self->{tables}{$key} = 1;
190             }
191             }
192              
193             sub scan_to_key {
194 1664     1664 0 2636 my $self = shift;
195 1664   100     4652 my $keys = shift // [ $self->get_keys ];
196 1664         3219 my $node = $self->{root};
197              
198             KEY:
199 1664         3886 for my $key (@$keys) {
200 989 100       2257 if (exists $node->{$key}) {
201 714         1448 my $ref = ref $node->{$key};
202 714 100       1550 if ( $ref eq 'HASH' ) {
203 569         913 $node = $node->{$key};
204 569         1097 next KEY;
205             }
206 145 100       356 if ( $ref eq 'ARRAY' ) {
207 142         279 $node = $node->{$key}[-1];
208 142         339 next KEY;
209             }
210 3         11 my $full_key = join '.', @$keys;
211 3         40 die "$full_key is already defined\n";
212             }
213             else {
214 275         834 $node = $node->{$key} = {};
215             }
216             }
217              
218 1661         3143 return $node;
219             }
220              
221             sub parse_table {
222 657     657 0 1410 my $self = shift;
223 657   100     2648 my $token = shift // $self->next_token // return; # may be undef on first token in empty document
      100        
224              
225 655         2659 $self->expect_type($token, 'table');
226 655         3141 $self->push_keys($token);
227 655         2214 $self->scan_to_key;
228              
229 653         2233 $self->declare_key($token);
230              
231 650         2024 TOKEN: while (my $token = $self->next_token) {
232 1483         2892 my $type = $token->{type};
233 1483 100       4093 next TOKEN if $type eq 'EOL';
234              
235 1175 100       2997 if ( $type eq 'key') {
236 902         2297 $self->expect_type($self->next_token, 'assign');
237 902         3496 $self->push_keys($token);
238 902         2314 $self->set_key($self->next_token);
239 834         128771 $self->pop_keys;
240              
241 834 100       1930 if (my $eol = $self->next_token) {
242 642         1603 $self->expect_type($eol, 'EOL');
243             } else {
244 135         837 return;
245             }
246 625         2945 next TOKEN;
247             }
248              
249 273 100       719 if ($type eq 'array_table') {
250 35         149 $self->pop_keys;
251 35         117 @_ = ($self, $token);
252 35         212 goto \&parse_array_table;
253             }
254              
255 238 100       572 if ( $type eq 'table') {
256 218         622 $self->pop_keys;
257 218         565 @_ = ($self, $token);
258 218         933 goto \&parse_table;
259             }
260              
261 20         134 $self->parse_error($token, "expected key-value pair, table, or array of tables but got $type");
262             }
263             }
264              
265             sub parse_array_table {
266 74     74 0 150 my $self = shift;
267 74   33     200 my $token = shift // $self->next_token;
268 74         261 $self->expect_type($token, 'array_table');
269 74         359 $self->push_keys($token);
270              
271 74         228 $self->declare_key($token);
272              
273 73         200 my @keys = $self->get_keys;
274 73         145 my $key = pop @keys;
275 73         219 my $node = $self->scan_to_key(\@keys);
276 73   100     387 $node->{$key} //= [];
277 73         126 push @{ $node->{$key} }, {};
  73         226  
278              
279             TOKEN:
280 71         199 while (my $token = $self->next_token) {
281 262         460 my $type = $token->{type};
282 262 100       675 next TOKEN if $type eq 'EOL';
283              
284 133 100       353 if ($type eq 'key') {
285 79         194 $self->expect_type($self->next_token, 'assign');
286 79         308 $self->push_keys($token);
287 79         193 $self->set_key($self->next_token);
288 79         234 $self->pop_keys;
289 79         296 next TOKEN;
290             }
291              
292 54 100       189 if ($type eq 'array_table') {
293 39         117 $self->pop_keys;
294 39         123 @_ = ($self, $token);
295 39         205 goto \&parse_array_table;
296             }
297              
298 15 50       67 if ($type eq 'table') {
299 15         47 $self->pop_keys;
300 15         65 @_ = ($self, $token);
301 15         68 goto \&parse_table;
302             }
303              
304 0         0 $self->parse_error($token, "expected key-value pair, table, or array of tables but got $type");
305             }
306             }
307              
308             sub parse_key {
309 0     0 0 0 my $self = shift;
310 0   0     0 my $token = shift // $self->next_token;
311 0         0 $self->expect_type($token, 'key');
312 0         0 return $token->{value};
313             }
314              
315             sub parse_value {
316 1314     1314 0 2243 my $self = shift;
317 1314         1957 my $token = shift;
318              
319 1314         2287 my $type = $token->{type};
320 1314 100       4072 return $token->{value} if $type eq 'string';
321 862 100       1992 return $self->inflate_float($token) if $type eq'float';
322 764 100       2410 return $self->inflate_integer($token) if $type eq 'integer';
323 353 100       913 return $self->{inflate_boolean}->($token->{value}) if $type eq 'bool';
324 316 100       751 return $self->parse_datetime($token) if $type eq 'datetime';
325 262 100       922 return $self->parse_inline_table($token) if $type eq 'inline_table';
326 166 100       776 return $self->parse_array($token) if $type eq 'inline_array';
327              
328 4         24 $self->parse_error($token, "value expected (bool, number, string, datetime, inline array, inline table), but found $type");
329             }
330              
331             #-------------------------------------------------------------------------------
332             # TOML permits a space instead of a T, which RFC3339 does not allow. TOML (at
333             # least, according to BurntSushi/toml-tests) allows z instead of Z, which
334             # RFC3339 also does not permit. We will be flexible and allow them both, but
335             # fix them up. TOML also specifies millisecond precision. If fractional seconds
336             # are specified. Whatever.
337             #-------------------------------------------------------------------------------
338             sub parse_datetime {
339 54     54 0 86 my $self = shift;
340 54         89 my $token = shift;
341 54         121 my $value = $token->{value};
342              
343             # Normalize
344 54         192 $value =~ tr/z/Z/;
345 54         129 $value =~ tr/ /T/;
346 54         176 $value =~ s/t/T/;
347 54         1725 $value =~ s/(\.\d+)($TimeOffset)$/sprintf(".%06d%s", $1 * 1000000, $2)/e;
  8         71  
348              
349 54         194 return $self->{inflate_datetime}->($value);
350             }
351              
352             sub parse_array {
353 162     162 0 342 my $self = shift;
354 162         283 my $token = shift;
355              
356 162         482 $self->declare_key($token);
357              
358 162         289 my @array;
359 162         362 my $expect = 'EOL|inline_array_close|string|float|integer|bool|datetime|inline_table|inline_array';
360              
361 162         242 TOKEN: while (1) {
362 939         2023 my $token = $self->next_token;
363 936         2365 $self->expect_type($token, $expect);
364              
365 933 100       3394 if ( $token->{type} eq 'comma') {
366 218         477 $expect = 'EOL|inline_array_close|string|float|integer|bool|datetime|inline_table|inline_array';
367 218         453 next TOKEN;
368             }
369              
370 715 100       1646 next TOKEN if $token->{type} eq 'EOL';
371 461 100       1147 last TOKEN if $token->{type} eq 'inline_array_close';
372              
373 307         879 push @array, $self->parse_value($token);
374 305         607 $expect = 'comma|EOL|inline_array_close';
375             }
376              
377 154         664 return \@array;
378             }
379              
380             sub parse_inline_table {
381 96     96 0 194 my $self = shift;
382 96         155 my $token = shift;
383              
384 96         203 my $table = {};
385 96         191 my $expect = 'EOL|inline_table_close|key';
386              
387 96         197 TOKEN: while (1) {
388 189         427 my $token = $self->next_token;
389 189         587 $self->expect_type($token, $expect);
390              
391 181         695 my $type = $token->{type};
392 181 100       563 if ($type eq 'comma') {
393             $expect = $self->{strict}
394 19 100       66 ? 'EOL|key'
395             : 'EOL|key|inline_table_close';
396              
397 19         51 next TOKEN;
398             }
399              
400 162 100       487 if ($type eq 'key') {
401 74         206 $self->expect_type($self->next_token, 'assign');
402              
403 74         178 my $node = $table;
404 74         118 my @keys = @{ $token->{value} };
  74         238  
405 74         142 my $key = pop @keys;
406              
407 74         178 for (@keys) {
408 16   100     85 $node->{$_} ||= {};
409 16         36 $node = $node->{$_};
410             }
411              
412 74 50       216 if (exists $node->{$key}) {
413 0         0 $self->parse_error($token, 'duplicate key: ' . join('.', map{ qq{"$_"} } @{ $token->{value} }));
  0         0  
  0         0  
414             } else {
415 74         193 $node->{ $key } = $self->parse_value($self->next_token);
416             }
417              
418 74         232 $expect = 'comma|inline_table_close';
419 74         277 next TOKEN;
420             }
421              
422 88 100       342 last TOKEN if $type eq 'inline_table_close';
423              
424 2         12 $self->parse_error($token, "inline table expected key-value pair, but found $type");
425             }
426              
427 86         417 return $table;
428             }
429              
430             sub inflate_float {
431 98     98 0 157 my $self = shift;
432 98         159 my $token = shift;
433 98         227 my $value = $token->{value};
434              
435             # Caller-defined inflation routine
436 98 100       235 if ($self->{inflate_float}) {
437 1         6 return $self->{inflate_float}->($value);
438             }
439              
440 97 100       435 return 'NaN' if $value =~ /^[-+]?nan$/i;
441 84 100       277 return 'inf' if $value =~ /^\+?inf$/i;
442 77 100       241 return '-inf' if $value =~ /^-inf$/i;
443              
444             # Not a bignum
445 73 100       610 if (0 + $value eq $value) {
446 38         209 return 0 + $value;
447             }
448              
449             #-----------------------------------------------------------------------------
450             # Scientific notation is a hairier situation. In order to determine whether a
451             # value will fit inside a perl svnv, we can't just coerce the value to a
452             # number and then test it against the string, because, for example, this will
453             # always be false:
454             #
455             # 9 eq "3e2"
456             #
457             # Instead, we are forced to test the coerced value against a BigFloat, which
458             # is capable of holding the number.
459             #-----------------------------------------------------------------------------
460 35 100       140 if ($value =~ /[eE]/) {
461 25 50       133 if (Math::BigFloat->new($value)->beq(0 + $value)) {
462 25         62260 return 0 + $value;
463             }
464             }
465              
466 10         70 return Math::BigFloat->new($value);
467             }
468              
469             sub inflate_integer {
470 411     411 0 672 my $self = shift;
471 411         624 my $token = shift;
472 411         852 my $value = $token->{value};
473              
474             # Caller-defined inflation routine
475 411 100       1039 if ($self->{inflate_integer}) {
476 1         4 return $self->{inflate_integer}->($value);
477             }
478              
479             # Hex
480 410 100       1210 if ($value =~ /^0x/) {
481 286     286   2786 no warnings 'portable';
  286         800  
  286         31497  
482 15         52 my $hex = hex $value;
483 15         131 my $big = Math::BigInt->new($value);
484 15 50       49634 return $big->beq($hex) ? $hex : $big;
485             }
486              
487             # Octal
488 395 100       1094 if ($value =~ /^0o/) {
489 286     286   2411 no warnings 'portable';
  286         840  
  286         34803  
490 11         63 $value =~ s/^0o/0/;
491 11         36 my $oct = oct $value;
492 11         64 my $big = Math::BigInt->from_oct($value);
493 11 50       46963 return $big->beq($oct) ? $oct : $big;
494             }
495              
496             # Binary
497 384 100       942 if ($value =~ /^0b/) {
498 286     286   2392 no warnings 'portable';
  286         780  
  286         36566  
499 10         35 my $bin = oct $value; # oct handles 0b as binary
500 10         73 my $big = Math::BigInt->new($value);
501 10 50       47226 return $big->beq($bin) ? $bin : $big;
502             }
503              
504             # Not a bignum
505 374 100       1369 if (0 + $value eq $value) {
506 368         1444 return 0 + $value;
507             }
508              
509 6         57 return Math::BigInt->new($value);
510             }
511              
512             1;
513              
514             __END__
515              
516             =pod
517              
518             =encoding UTF-8
519              
520             =head1 NAME
521              
522             TOML::Tiny::Parser - parser used by TOML::Tiny
523              
524             =head1 VERSION
525              
526             version 0.16
527              
528             =head1 AUTHOR
529              
530             Jeff Ober <sysread@fastmail.fm>
531              
532             =head1 COPYRIGHT AND LICENSE
533              
534             This software is copyright (c) 2023 by Jeff Ober.
535              
536             This is free software; you can redistribute it and/or modify it under
537             the same terms as the Perl 5 programming language system itself.
538              
539             =cut