File Coverage

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


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.20';
4 287     287   214160 use utf8;
  287         526  
  287         2133  
5 287     287   12045 use strict;
  287         509  
  287         9808  
6 287     287   1553 use warnings;
  287         591  
  287         16214  
7 287     287   1604 no warnings qw(experimental);
  287         633  
  287         10565  
8 287     287   3234 use v5.18;
  287         1013  
9              
10 287     287   1771 use Carp qw(confess);
  287         897  
  287         20746  
11 287     287   118721 use Data::Dumper qw(Dumper);
  287         1603985  
  287         25779  
12 287     287   2369 use Encode qw(decode FB_CROAK);
  287         586  
  287         15996  
13 287     287   259111 use Math::BigFloat ();
  287         17700075  
  287         10808  
14 287     287   3270 use Math::BigInt ();
  287         853  
  287         10232  
15 287     287   168823 use TOML::Tiny::Grammar qw($TimeOffset);
  287         1258  
  287         64461  
16 287     287   198592 use TOML::Tiny::Tokenizer ();
  287         1184  
  287         994491  
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 433     433 0 784513 my ($class, %param) = @_;
29             bless{
30             inflate_integer => $param{inflate_integer},
31             inflate_float => $param{inflate_float},
32 53     53   247 inflate_datetime => $param{inflate_datetime} || sub{ shift },
33 37 100   37   253 inflate_boolean => $param{inflate_boolean} || sub{ shift eq 'true' ? $TRUE : $FALSE },
34             strict => $param{strict},
35 433   66     11394 }, $class;
      66        
36             }
37              
38             sub next_token {
39 6522     6522 0 11795 my $self = shift;
40 6522   66     26097 my $token = $self->{tokenizer} && $self->{tokenizer}->next_token;
41 6408         19012 return $token;
42             }
43              
44             sub parse {
45 433     433 0 1411 my ($self, $toml) = @_;
46              
47 433 100       3463 if ($self->{strict}) {
48 185         1885 $toml = decode('UTF-8', "$toml", FB_CROAK);
49             }
50              
51 427         12072 $self->{tokenizer} = TOML::Tiny::Tokenizer->new(source => $toml);
52 427         1588 $self->{keys} = [];
53 427         1544 $self->{root} = {};
54 427         1215 $self->{tables} = {}; # "seen" hash of explicitly defined table names (e.g. [foo])
55 427         1283 $self->{arrays} = {}; # "seen" hash of explicitly defined static arrays (e.g. foo=[])
56 427         1358 $self->{array_tables} = {}; # "seen" hash of explicitly defined arrays of tables (e.g. [[foo]])
57              
58 427         2703 $self->parse_table;
59 248         802 my $result = $self->{root};
60              
61 248         1841 delete $self->{tokenizer};
62 248         688 delete $self->{keys};
63 248         563 delete $self->{root};
64 248         665 delete $self->{tables};
65 248         559 delete $self->{arrays};
66 248         498 delete $self->{array_tables};
67              
68 248         1084 return $result;
69             }
70              
71             sub parse_error {
72 60     60 0 239 my ($self, $token, $msg) = @_;
73 60 100       251 my $line = $token ? $token->{line} : 'EOF';
74 60 50       304 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         825 die "toml parse error at line $line: $msg\n";
97             }
98             }
99              
100             sub expect_type {
101 3638     3638 0 7874 my ($self, $token, $expected) = @_;
102 3638 100       8687 my $actual = $token ? $token->{type} : 'EOF';
103 3638 100       81891 $self->parse_error($token, "expected $expected, but found $actual")
104             unless $actual =~ /$expected/;
105             }
106              
107              
108             sub current_key {
109 907     907 0 1574 my $self = shift;
110 907         2305 my @keys = $self->get_keys;
111 907         2386 my $key = join '.', map{ qq{"$_"} } @keys;
  765         2335  
112 907         3516 return $key;
113             }
114              
115             sub push_keys {
116 1755     1755 0 3527 my ($self, $token) = @_;
117 1755         2618 push @{ $self->{keys} }, $token->{value};
  1755         4994  
118             }
119              
120             sub pop_keys {
121 1262     1262 0 1989 my $self = shift;
122 1262         1720 pop @{ $self->{keys} };
  1262         2804  
123             }
124              
125             sub get_keys {
126 2625     2625 0 3939 my $self = shift;
127 2625         3660 return map{ @$_ } @{ $self->{keys} };
  3756         10866  
  2625         6079  
128             }
129              
130             sub set_key {
131 966     966 0 2227 my ($self, $token) = @_;
132 966         3209 my @keys = $self->get_keys;
133 966         2565 my $key = pop @keys;
134 966         3166 my $node = $self->scan_to_key(\@keys);
135              
136 965 100 100     4792 if ($key && exists $node->{$key}) {
137 2         6 $self->parse_error($token, 'duplicate key: ' . $self->current_key);
138             }
139              
140 963         3117 $node->{$key} = $self->parse_value($token);
141             }
142              
143             sub declare_key {
144 905     905 0 2009 my ($self, $token) = @_;
145 905   100     2727 my $key = $self->current_key || return;
146              
147 480 100       1540 if ($token->{type} eq 'inline_array') {
148             $self->parse_error($token, "duplicate key: $key")
149 163 50       453 if exists $self->{array_tables}{$key};
150              
151 163         505 $self->{arrays}{$key} = 1;
152 163         303 return;
153             }
154              
155 317 100       788 if ($token->{type} eq 'array_table') {
156 75 100       221 if (exists $self->{arrays}{$key}) {
157 1         6 $self->parse_error($token, "duplicate key: $key");
158             }
159              
160 74         225 $self->{array_tables}{$key} = 1;
161 74         185 return;
162             }
163              
164 242 50       657 if ($token->{type} eq 'table') {
165             $self->parse_error($token, "duplicate key: $key")
166             if exists $self->{arrays}{$key}
167 242 100 66     1310 || exists $self->{array_tables}{$key};
168              
169 240 100       603 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         21 my $in_a_stupid_table_array = 0;
173 9         48 my $node = $self->{root};
174              
175 9         57 for my $key ($self->get_keys) {
176 9 100 66     71 if (exists $node->{$key} && ref($node->{$key}) eq 'ARRAY') {
177 8         16 $in_a_stupid_table_array = 1;
178 8         18 last;
179             } else {
180 1         3 $node = $node->{$key};
181             }
182             }
183              
184 9 100       32 unless ($in_a_stupid_table_array) {
185 1         7 $self->parse_error($token, "duplicate key: $key");
186             }
187 8         22 return;
188             }
189 231         714 $self->{tables}{$key} = 1;
190             }
191             }
192              
193             sub scan_to_key {
194 1709     1709 0 2685 my $self = shift;
195 1709   100     6334 my $keys = shift // [ $self->get_keys ];
196 1709         3306 my $node = $self->{root};
197              
198             KEY:
199 1709         3966 for my $key (@$keys) {
200 1040 100       2211 if (exists $node->{$key}) {
201 747         1505 my $ref = ref $node->{$key};
202 747 100       1584 if ( $ref eq 'HASH' ) {
203 601         1020 $node = $node->{$key};
204 601         1223 next KEY;
205             }
206 146 100       333 if ( $ref eq 'ARRAY' ) {
207 143         266 $node = $node->{$key}[-1];
208 143         335 next KEY;
209             }
210 3         12 my $full_key = join '.', @$keys;
211 3         31 die "$full_key is already defined\n";
212             }
213             else {
214 293         897 $node = $node->{$key} = {};
215             }
216             }
217              
218 1706         3475 return $node;
219             }
220              
221             sub parse_table {
222 671     671 0 1336 my $self = shift;
223 671   100     3531 my $token = shift // $self->next_token // return; # may be undef on first token in empty document
      100        
224              
225 669         3083 $self->expect_type($token, 'table');
226 669         3081 $self->push_keys($token);
227 669         2668 $self->scan_to_key;
228              
229 667         2682 $self->declare_key($token);
230              
231 664         2043 TOKEN: while (my $token = $self->next_token) {
232 1535         3601 my $type = $token->{type};
233 1535 100       4395 next TOKEN if $type eq 'EOL';
234              
235 1216 100       3188 if ( $type eq 'key') {
236 931         2539 $self->expect_type($self->next_token, 'assign');
237 931         3264 $self->push_keys($token);
238 931         2295 $self->set_key($self->next_token);
239 863         191564 $self->pop_keys;
240              
241 863 100       2022 if (my $eol = $self->next_token) {
242 670         1670 $self->expect_type($eol, 'EOL');
243             } else {
244 136         609 return;
245             }
246 653         3358 next TOKEN;
247             }
248              
249 285 100       704 if ($type eq 'array_table') {
250 36         148 $self->pop_keys;
251 36         110 @_ = ($self, $token);
252 36         229 goto \&parse_array_table;
253             }
254              
255 249 100       558 if ( $type eq 'table') {
256 229         701 $self->pop_keys;
257 229         605 @_ = ($self, $token);
258 229         1157 goto \&parse_table;
259             }
260              
261 20         145 $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 75     75 0 143 my $self = shift;
267 75   33     199 my $token = shift // $self->next_token;
268 75         261 $self->expect_type($token, 'array_table');
269 75         251 $self->push_keys($token);
270              
271 75         222 $self->declare_key($token);
272              
273 74         176 my @keys = $self->get_keys;
274 74         172 my $key = pop @keys;
275 74         323 my $node = $self->scan_to_key(\@keys);
276 74   100     405 $node->{$key} //= [];
277 74         189 push @{ $node->{$key} }, {};
  74         224  
278              
279             TOKEN:
280 72         274 while (my $token = $self->next_token) {
281 264         421 my $type = $token->{type};
282 264 100       716 next TOKEN if $type eq 'EOL';
283              
284 134 100       318 if ($type eq 'key') {
285 80         201 $self->expect_type($self->next_token, 'assign');
286 80         256 $self->push_keys($token);
287 80         172 $self->set_key($self->next_token);
288 80         242 $self->pop_keys;
289 80         313 next TOKEN;
290             }
291              
292 54 100       154 if ($type eq 'array_table') {
293 39         120 $self->pop_keys;
294 39         131 @_ = ($self, $token);
295 39         191 goto \&parse_array_table;
296             }
297              
298 15 50       63 if ($type eq 'table') {
299 15         84 $self->pop_keys;
300 15         54 @_ = ($self, $token);
301 15         222 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 1348     1348 0 2214 my $self = shift;
317 1348         2026 my $token = shift;
318              
319 1348         2395 my $type = $token->{type};
320 1348 100       4354 return $token->{value} if $type eq 'string';
321 877 100       2243 return $self->inflate_float($token) if $type eq'float';
322 778 100       3229 return $self->inflate_integer($token) if $type eq 'integer';
323 360 100       1073 return $self->{inflate_boolean}->($token->{value}) if $type eq 'bool';
324 321 100       966 return $self->parse_datetime($token) if $type eq 'datetime';
325 265 100       962 return $self->parse_inline_table($token) if $type eq 'inline_table';
326 167 100       730 return $self->parse_array($token) if $type eq 'inline_array';
327              
328 4         26 $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 56     56 0 90 my $self = shift;
340 56         88 my $token = shift;
341 56         106 my $value = $token->{value};
342              
343             # Normalize
344 56         162 $value =~ tr/z/Z/;
345 56         129 $value =~ tr/ /T/;
346 56         203 $value =~ s/t/T/;
347 56         2899 $value =~ s/(\.\d+)($TimeOffset)$/sprintf(".%09d%s", $1 * 1000000000, $2)/e;
  9         91  
348              
349 56         211 return $self->{inflate_datetime}->($value);
350             }
351              
352             sub parse_array {
353 163     163 0 298 my $self = shift;
354 163         283 my $token = shift;
355              
356 163         502 $self->declare_key($token);
357              
358 163         217 my @array;
359 163         244 my $expect = 'EOL|inline_array_close|string|float|integer|bool|datetime|inline_table|inline_array';
360              
361 163         221 TOKEN: while (1) {
362 947         2056 my $token = $self->next_token;
363 944         2432 $self->expect_type($token, $expect);
364              
365 941 100       3253 if ( $token->{type} eq 'comma') {
366 220         374 $expect = 'EOL|inline_array_close|string|float|integer|bool|datetime|inline_table|inline_array';
367 220         460 next TOKEN;
368             }
369              
370 721 100       1654 next TOKEN if $token->{type} eq 'EOL';
371 464 100       988 last TOKEN if $token->{type} eq 'inline_array_close';
372              
373 309         827 push @array, $self->parse_value($token);
374 307         602 $expect = 'comma|EOL|inline_array_close';
375             }
376              
377 155         742 return \@array;
378             }
379              
380             sub parse_inline_table {
381 98     98 0 194 my $self = shift;
382 98         147 my $token = shift;
383              
384 98         165 my $table = {};
385 98         170 my $expect = 'EOL|inline_table_close|key';
386              
387 98         146 TOKEN: while (1) {
388 193         483 my $token = $self->next_token;
389 193         548 $self->expect_type($token, $expect);
390              
391 185         533 my $type = $token->{type};
392 185 100       466 if ($type eq 'comma') {
393             $expect = $self->{strict}
394 19 100       57 ? 'EOL|key'
395             : 'EOL|key|inline_table_close';
396              
397 19         40 next TOKEN;
398             }
399              
400 166 100       370 if ($type eq 'key') {
401 76         243 $self->expect_type($self->next_token, 'assign');
402              
403 76         150 my $node = $table;
404 76         128 my @keys = @{ $token->{value} };
  76         219  
405 76         185 my $key = pop @keys;
406              
407 76         196 for (@keys) {
408 16   100     58 $node->{$_} ||= {};
409 16         23 $node = $node->{$_};
410             }
411              
412 76 50       184 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 76         177 $node->{ $key } = $self->parse_value($self->next_token);
416             }
417              
418 76         162 $expect = 'comma|inline_table_close';
419 76         357 next TOKEN;
420             }
421              
422 90 100       258 last TOKEN if $type eq 'inline_table_close';
423              
424 2         13 $self->parse_error($token, "inline table expected key-value pair, but found $type");
425             }
426              
427 88         337 return $table;
428             }
429              
430             sub inflate_float {
431 99     99 0 180 my $self = shift;
432 99         173 my $token = shift;
433 99         195 my $value = $token->{value};
434              
435             # Caller-defined inflation routine
436 99 100       273 if ($self->{inflate_float}) {
437 1         4 return $self->{inflate_float}->($value);
438             }
439              
440 98 100       586 return 'NaN' if $value =~ /^[-+]?nan$/i;
441 85 100       366 return 'inf' if $value =~ /^\+?inf$/i;
442 78 100       258 return '-inf' if $value =~ /^-inf$/i;
443              
444             # Not a bignum
445 74 100       751 if (0 + $value eq $value) {
446 39         215 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       178 if ($value =~ /[eE]/) {
461 25 50       176 if (Math::BigFloat->new($value)->beq(0 + $value)) {
462 25         72833 return 0 + $value;
463             }
464             }
465              
466 10         94 return Math::BigFloat->new($value);
467             }
468              
469             sub inflate_integer {
470 418     418 0 657 my $self = shift;
471 418         612 my $token = shift;
472 418         850 my $value = $token->{value};
473              
474             # Caller-defined inflation routine
475 418 100       1184 if ($self->{inflate_integer}) {
476 1         5 return $self->{inflate_integer}->($value);
477             }
478              
479             # Hex
480 417 100       1251 if ($value =~ /^0x/) {
481 287     287   3066 no warnings 'portable';
  287         674  
  287         41717  
482 16         47 my $hex = hex $value;
483 16         88 my $big = Math::BigInt->new($value);
484 16 50       64430 return $big->beq($hex) ? $hex : $big;
485             }
486              
487             # Octal
488 401 100       982 if ($value =~ /^0o/) {
489 287     287   2058 no warnings 'portable';
  287         636  
  287         36956  
490 12         49 $value =~ s/^0o/0/;
491 12         38 my $oct = oct $value;
492 12         77 my $big = Math::BigInt->from_oct($value);
493 12 50       53803 return $big->beq($oct) ? $oct : $big;
494             }
495              
496             # Binary
497 389 100       999 if ($value =~ /^0b/) {
498 287     287   1989 no warnings 'portable';
  287         632  
  287         48581  
499 11         49 my $bin = oct $value; # oct handles 0b as binary
500 11         74 my $big = Math::BigInt->new($value);
501 11 50       77105 return $big->beq($bin) ? $bin : $big;
502             }
503              
504             # Not a bignum
505 378 100       1462 if (0 + $value eq $value) {
506 371         1580 return 0 + $value;
507             }
508              
509 7         84 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.20
527              
528             =head1 AUTHOR
529              
530             Jeff Ober <sysread@fastmail.fm>
531              
532             =head1 COPYRIGHT AND LICENSE
533              
534             This software is copyright (c) 2025 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