]> git.proxmox.com Git - pve-common.git/blame - src/PVE/JSONSchema.pm
sendmail: use more complete email regex and shellquote
[pve-common.git] / src / PVE / JSONSchema.pm
CommitLineData
e143e9d8
DM
1package PVE::JSONSchema;
2
e143e9d8 3use strict;
c36f332e 4use warnings;
e143e9d8
DM
5use Storable; # for dclone
6use Getopt::Long;
24197a9f
DM
7use Encode::Locale;
8use Encode;
e143e9d8 9use Devel::Cycle -quiet; # todo: remove?
e272bcb7 10use PVE::Tools qw(split_list $IPV6RE $IPV4RE);
e143e9d8
DM
11use PVE::Exception qw(raise);
12use HTTP::Status qw(:constants);
23b56245 13use Net::IP qw(:PROC);
bf27456b 14use Data::Dumper;
e143e9d8
DM
15
16use base 'Exporter';
17
18our @EXPORT_OK = qw(
e143e9d8 19get_standard_option
79ac628b
TL
20parse_property_string
21register_standard_option
e143e9d8
DM
22);
23
a8f93334
DJ
24our $CONFIGID_RE = qr/[a-z][a-z0-9_-]+/i;
25
9bbc4e17 26# Note: This class implements something similar to JSON schema, but it is not 100% complete.
e143e9d8
DM
27# see: http://tools.ietf.org/html/draft-zyp-json-schema-02
28# see: http://json-schema.org/
29
30# the code is similar to the javascript parser from http://code.google.com/p/jsonschema/
31
32my $standard_options = {};
33sub register_standard_option {
34 my ($name, $schema) = @_;
35
9bbc4e17 36 die "standard option '$name' already registered\n"
e143e9d8
DM
37 if $standard_options->{$name};
38
39 $standard_options->{$name} = $schema;
40}
41
42sub get_standard_option {
43 my ($name, $base) = @_;
44
45 my $std = $standard_options->{$name};
3432ae0c 46 die "no such standard option '$name'\n" if !$std;
e143e9d8
DM
47
48 my $res = $base || {};
49
50 foreach my $opt (keys %$std) {
c38ac70f 51 next if defined($res->{$opt});
e143e9d8
DM
52 $res->{$opt} = $std->{$opt};
53 }
54
55 return $res;
56};
57
58register_standard_option('pve-vmid', {
59 description => "The (unique) ID of the VM.",
60 type => 'integer', format => 'pve-vmid',
61 minimum => 1
62});
63
64register_standard_option('pve-node', {
65 description => "The cluster node name.",
66 type => 'string', format => 'pve-node',
67});
68
69register_standard_option('pve-node-list', {
70 description => "List of cluster node names.",
71 type => 'string', format => 'pve-node-list',
72});
73
74register_standard_option('pve-iface', {
75 description => "Network interface name.",
76 type => 'string', format => 'pve-iface',
77 minLength => 2, maxLength => 20,
78});
79
28a2669d 80register_standard_option('pve-storage-id', {
05e787c5
DM
81 description => "The storage identifier.",
82 type => 'string', format => 'pve-storage-id',
9bbc4e17 83});
05e787c5 84
28a2669d 85register_standard_option('pve-config-digest', {
dc5eae7d
DM
86 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
87 type => 'string',
88 optional => 1,
fb3a1b29 89 maxLength => 40, # sha1 hex digest length is 40
dc5eae7d
DM
90});
91
26bcdf92
WB
92register_standard_option('skiplock', {
93 description => "Ignore locks - only root is allowed to use this option.",
94 type => 'boolean',
95 optional => 1,
96});
97
28a2669d 98register_standard_option('extra-args', {
5851be88
WB
99 description => "Extra arguments as array",
100 type => 'array',
101 items => { type => 'string' },
102 optional => 1
103});
104
b21cf575
TL
105register_standard_option('fingerprint-sha256', {
106 description => "Certificate SHA 256 fingerprint.",
107 type => 'string',
108 pattern => '([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}',
109});
110
ac15655f
DM
111register_standard_option('pve-output-format', {
112 type => 'string',
113 description => 'Output format.',
ac6c61bf 114 enum => [ 'text', 'json', 'json-pretty', 'yaml' ],
ac15655f
DM
115 optional => 1,
116 default => 'text',
117});
118
98d5b8cb
TL
119register_standard_option('pve-snapshot-name', {
120 description => "The name of the snapshot.",
121 type => 'string', format => 'pve-configid',
122 maxLength => 40,
123});
124
e143e9d8 125my $format_list = {};
70fdc050 126my $format_validators = {};
e143e9d8
DM
127
128sub register_format {
70fdc050 129 my ($name, $format, $validator) = @_;
e143e9d8 130
70fdc050
SR
131 die "JSON schema format '$name' already registered\n"
132 if $format_list->{$name};
e143e9d8 133
70fdc050
SR
134 if ($validator) {
135 die "A \$validator function can only be specified for hash-based formats\n"
136 if ref($format) ne 'HASH';
137 $format_validators->{$name} = $validator;
138 }
139
140 $format_list->{$name} = $format;
e143e9d8
DM
141}
142
2421fba1 143sub get_format {
70fdc050
SR
144 my ($name) = @_;
145 return $format_list->{$name};
2421fba1
WB
146}
147
b5212042
DM
148my $renderer_hash = {};
149
150sub register_renderer {
151 my ($name, $code) = @_;
152
153 die "renderer '$name' already registered\n"
154 if $renderer_hash->{$name};
155
156 $renderer_hash->{$name} = $code;
157}
158
159sub get_renderer {
160 my ($name) = @_;
161 return $renderer_hash->{$name};
162}
163
e143e9d8 164# register some common type for pve
8ba7c72b
DM
165
166register_format('string', sub {}); # allow format => 'string-list'
167
c77b4c96
WB
168register_format('urlencoded', \&pve_verify_urlencoded);
169sub pve_verify_urlencoded {
170 my ($text, $noerr) = @_;
171 if ($text !~ /^[-%a-zA-Z0-9_.!~*'()]*$/) {
172 return undef if $noerr;
173 die "invalid urlencoded string: $text\n";
174 }
175 return $text;
176}
177
e143e9d8
DM
178register_format('pve-configid', \&pve_verify_configid);
179sub pve_verify_configid {
180 my ($id, $noerr) = @_;
9bbc4e17 181
a8f93334 182 if ($id !~ m/^$CONFIGID_RE$/) {
e143e9d8 183 return undef if $noerr;
9bbc4e17 184 die "invalid configuration ID '$id'\n";
e143e9d8
DM
185 }
186 return $id;
187}
188
05e787c5
DM
189PVE::JSONSchema::register_format('pve-storage-id', \&parse_storage_id);
190sub parse_storage_id {
191 my ($storeid, $noerr) = @_;
192
731950fd
WL
193 return parse_id($storeid, 'storage', $noerr);
194}
195
a8117ff3
WL
196PVE::JSONSchema::register_format('acme-plugin-id', \&parse_acme_plugin_id);
197sub parse_acme_plugin_id {
198 my ($pluginid, $noerr) = @_;
199
200 return parse_id($pluginid, 'ACME plugin', $noerr);
201}
202
731950fd
WL
203sub parse_id {
204 my ($id, $type, $noerr) = @_;
205
206 if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
05e787c5 207 return undef if $noerr;
731950fd 208 die "$type ID '$id' contains illegal characters\n";
05e787c5 209 }
731950fd 210 return $id;
05e787c5
DM
211}
212
e143e9d8
DM
213register_format('pve-vmid', \&pve_verify_vmid);
214sub pve_verify_vmid {
215 my ($vmid, $noerr) = @_;
216
50ae94c9 217 if ($vmid !~ m/^[1-9][0-9]{2,8}$/) {
e143e9d8
DM
218 return undef if $noerr;
219 die "value does not look like a valid VM ID\n";
220 }
221 return $vmid;
222}
223
224register_format('pve-node', \&pve_verify_node_name);
225sub pve_verify_node_name {
226 my ($node, $noerr) = @_;
227
e6db55c0 228 if ($node !~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)$/) {
e143e9d8
DM
229 return undef if $noerr;
230 die "value does not look like a valid node name\n";
231 }
232 return $node;
233}
234
18f93ddf
FG
235sub parse_idmap {
236 my ($idmap, $idformat) = @_;
237
238 return undef if !$idmap;
239
240 my $map = {};
241
242 foreach my $entry (PVE::Tools::split_list($idmap)) {
243 if ($entry eq '1') {
244 $map->{identity} = 1;
245 } elsif ($entry =~ m/^([^:]+):([^:]+)$/) {
246 my ($source, $target) = ($1, $2);
247 eval {
e12df964
TL
248 check_format($idformat, $source, '');
249 check_format($idformat, $target, '');
18f93ddf 250 };
e12df964 251 die "entry '$entry' contains invalid ID - $@\n" if $@;
18f93ddf
FG
252
253 die "duplicate mapping for source '$source'\n"
e12df964 254 if exists $map->{entries}->{$source};
18f93ddf
FG
255
256 $map->{entries}->{$source} = $target;
257 } else {
258 eval {
e12df964 259 check_format($idformat, $entry);
18f93ddf 260 };
e12df964 261 die "entry '$entry' contains invalid ID - $@\n" if $@;
18f93ddf
FG
262
263 die "default target ID can only be provided once\n"
e12df964 264 if exists $map->{default};
18f93ddf
FG
265
266 $map->{default} = $entry;
267 }
268 }
269
270 die "identity mapping cannot be combined with other mappings\n"
e12df964 271 if $map->{identity} && ($map->{default} || exists $map->{entries});
18f93ddf
FG
272
273 return $map;
274}
275
276register_format('storagepair', \&verify_storagepair);
277sub verify_storagepair {
278 my ($storagepair, $noerr) = @_;
279
280 # note: this only checks a single list entry
281 # when using a storagepair-list map, you need to pass the full
282 # parameter to parse_idmap
283 eval { parse_idmap($storagepair, 'pve-storage-id') };
284 if ($@) {
285 return undef if $noerr;
286 die "$@\n";
287 }
288
289 return $storagepair;
290}
291
14324ea8
CE
292register_format('mac-addr', \&pve_verify_mac_addr);
293sub pve_verify_mac_addr {
294 my ($mac_addr, $noerr) = @_;
295
4fdf30c4
TL
296 # don't allow I/G bit to be set, most of the time it breaks things, see:
297 # https://pve.proxmox.com/pipermail/pve-devel/2019-March/035998.html
a750d596 298 if ($mac_addr !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$/i) {
14324ea8 299 return undef if $noerr;
a750d596 300 die "value does not look like a valid unicast MAC address\n";
14324ea8
CE
301 }
302 return $mac_addr;
a750d596 303
14324ea8 304}
a750d596
SI
305register_standard_option('mac-addr', {
306 type => 'string',
307 description => 'Unicast MAC address.',
4fdf30c4 308 verbose_description => 'A common MAC address with the I/G (Individual/Group) bit not set.',
a750d596
SI
309 format_description => "XX:XX:XX:XX:XX:XX",
310 optional => 1,
311 format => 'mac-addr',
312});
14324ea8 313
e143e9d8
DM
314register_format('ipv4', \&pve_verify_ipv4);
315sub pve_verify_ipv4 {
316 my ($ipv4, $noerr) = @_;
317
ed5880ac
DM
318 if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
319 return undef if $noerr;
320 die "value does not look like a valid IPv4 address\n";
e143e9d8
DM
321 }
322 return $ipv4;
323}
a13c6f08 324
ed5880ac 325register_format('ipv6', \&pve_verify_ipv6);
93276209 326sub pve_verify_ipv6 {
ed5880ac
DM
327 my ($ipv6, $noerr) = @_;
328
329 if ($ipv6 !~ m/^(?:$IPV6RE)$/) {
330 return undef if $noerr;
331 die "value does not look like a valid IPv6 address\n";
332 }
333 return $ipv6;
334}
335
336register_format('ip', \&pve_verify_ip);
337sub pve_verify_ip {
338 my ($ip, $noerr) = @_;
339
340 if ($ip !~ m/^(?:(?:$IPV4RE)|(?:$IPV6RE))$/) {
341 return undef if $noerr;
342 die "value does not look like a valid IP address\n";
343 }
344 return $ip;
345}
346
283ac2ba
DC
347PVE::JSONSchema::register_format('ldap-simple-attr', \&verify_ldap_simple_attr);
348sub verify_ldap_simple_attr {
349 my ($attr, $noerr) = @_;
350
351 if ($attr =~ m/^[a-zA-Z0-9]+$/) {
352 return $attr;
353 }
354
355 die "value '$attr' does not look like a simple ldap attribute name\n" if !$noerr;
356
357 return undef;
358}
359
a13c6f08 360my $ipv4_mask_hash = {
aad3582e 361 '0.0.0.0' => 0,
a13c6f08
DM
362 '128.0.0.0' => 1,
363 '192.0.0.0' => 2,
364 '224.0.0.0' => 3,
365 '240.0.0.0' => 4,
366 '248.0.0.0' => 5,
367 '252.0.0.0' => 6,
368 '254.0.0.0' => 7,
369 '255.0.0.0' => 8,
370 '255.128.0.0' => 9,
371 '255.192.0.0' => 10,
372 '255.224.0.0' => 11,
373 '255.240.0.0' => 12,
374 '255.248.0.0' => 13,
375 '255.252.0.0' => 14,
376 '255.254.0.0' => 15,
377 '255.255.0.0' => 16,
378 '255.255.128.0' => 17,
379 '255.255.192.0' => 18,
380 '255.255.224.0' => 19,
381 '255.255.240.0' => 20,
382 '255.255.248.0' => 21,
383 '255.255.252.0' => 22,
384 '255.255.254.0' => 23,
385 '255.255.255.0' => 24,
386 '255.255.255.128' => 25,
387 '255.255.255.192' => 26,
388 '255.255.255.224' => 27,
389 '255.255.255.240' => 28,
390 '255.255.255.248' => 29,
e43faad9
WB
391 '255.255.255.252' => 30,
392 '255.255.255.254' => 31,
393 '255.255.255.255' => 32,
a13c6f08
DM
394};
395
aad3582e
DC
396sub get_netmask_bits {
397 my ($mask) = @_;
398 return $ipv4_mask_hash->{$mask};
399}
400
e143e9d8
DM
401register_format('ipv4mask', \&pve_verify_ipv4mask);
402sub pve_verify_ipv4mask {
403 my ($mask, $noerr) = @_;
404
a13c6f08 405 if (!defined($ipv4_mask_hash->{$mask})) {
e143e9d8
DM
406 return undef if $noerr;
407 die "value does not look like a valid IP netmask\n";
408 }
409 return $mask;
410}
411
703c1f88
WB
412register_format('CIDRv6', \&pve_verify_cidrv6);
413sub pve_verify_cidrv6 {
e272bcb7
DM
414 my ($cidr, $noerr) = @_;
415
70ea2250 416 if ($cidr =~ m!^(?:$IPV6RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 128)) {
e272bcb7 417 return $cidr;
703c1f88
WB
418 }
419
420 return undef if $noerr;
421 die "value does not look like a valid IPv6 CIDR network\n";
422}
423
424register_format('CIDRv4', \&pve_verify_cidrv4);
425sub pve_verify_cidrv4 {
426 my ($cidr, $noerr) = @_;
427
0526cc2d 428 if ($cidr =~ m!^(?:$IPV4RE)(?:/(\d+))$! && ($1 > 7) && ($1 <= 32)) {
e272bcb7
DM
429 return $cidr;
430 }
431
432 return undef if $noerr;
703c1f88
WB
433 die "value does not look like a valid IPv4 CIDR network\n";
434}
435
436register_format('CIDR', \&pve_verify_cidr);
437sub pve_verify_cidr {
438 my ($cidr, $noerr) = @_;
439
440 if (!(pve_verify_cidrv4($cidr, 1) ||
441 pve_verify_cidrv6($cidr, 1)))
442 {
443 return undef if $noerr;
444 die "value does not look like a valid CIDR network\n";
445 }
446
447 return $cidr;
448}
449
450register_format('pve-ipv4-config', \&pve_verify_ipv4_config);
451sub pve_verify_ipv4_config {
452 my ($config, $noerr) = @_;
453
454 return $config if $config =~ /^(?:dhcp|manual)$/ ||
455 pve_verify_cidrv4($config, 1);
456 return undef if $noerr;
457 die "value does not look like a valid ipv4 network configuration\n";
458}
459
460register_format('pve-ipv6-config', \&pve_verify_ipv6_config);
461sub pve_verify_ipv6_config {
462 my ($config, $noerr) = @_;
463
464 return $config if $config =~ /^(?:auto|dhcp|manual)$/ ||
465 pve_verify_cidrv6($config, 1);
466 return undef if $noerr;
467 die "value does not look like a valid ipv6 network configuration\n";
e272bcb7
DM
468}
469
e143e9d8
DM
470register_format('email', \&pve_verify_email);
471sub pve_verify_email {
472 my ($email, $noerr) = @_;
473
4c4bd104 474 if ($email !~ /^$PVE::Tools::EMAIL_RE$/) {
e143e9d8
DM
475 return undef if $noerr;
476 die "value does not look like a valid email address\n";
477 }
478 return $email;
479}
480
34ebb226
DM
481register_format('dns-name', \&pve_verify_dns_name);
482sub pve_verify_dns_name {
483 my ($name, $noerr) = @_;
484
ce33e978 485 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
34ebb226
DM
486
487 if ($name !~ /^(${namere}\.)*${namere}$/) {
488 return undef if $noerr;
489 die "value does not look like a valid DNS name\n";
490 }
491 return $name;
492}
493
e76308e6
OB
494register_format('timezone', \&pve_verify_timezone);
495sub pve_verify_timezone {
496 my ($timezone, $noerr) = @_;
497
e76308e6 498 return $timezone if $timezone eq 'UTC';
36b9c073
TL
499
500 open(my $fh, "<", "/usr/share/zoneinfo/zone.tab");
501 while (my $line = <$fh>) {
502 next if $line =~ /^\s*#/;
e76308e6 503 chomp $line;
36b9c073
TL
504 my $zone = (split /\t/, $line)[2];
505 return $timezone if $timezone eq $zone; # found
e76308e6
OB
506 }
507 close $fh;
508
509 return undef if $noerr;
510 die "invalid time zone '$timezone'\n";
e76308e6
OB
511}
512
e143e9d8
DM
513# network interface name
514register_format('pve-iface', \&pve_verify_iface);
515sub pve_verify_iface {
516 my ($id, $noerr) = @_;
9bbc4e17 517
e143e9d8
DM
518 if ($id !~ m/^[a-z][a-z0-9_]{1,20}([:\.]\d+)?$/i) {
519 return undef if $noerr;
9bbc4e17 520 die "invalid network interface name '$id'\n";
e143e9d8
DM
521 }
522 return $id;
523}
524
d07b7084
WB
525# general addresses by name or IP
526register_format('address', \&pve_verify_address);
527sub pve_verify_address {
528 my ($addr, $noerr) = @_;
529
530 if (!(pve_verify_ip($addr, 1) ||
531 pve_verify_dns_name($addr, 1)))
532 {
533 return undef if $noerr;
534 die "value does not look like a valid address: $addr\n";
535 }
536 return $addr;
537}
538
b944a22a
WB
539register_format('disk-size', \&pve_verify_disk_size);
540sub pve_verify_disk_size {
541 my ($size, $noerr) = @_;
542 if (!defined(parse_size($size))) {
543 return undef if $noerr;
544 die "value does not look like a valid disk size: $size\n";
545 }
546 return $size;
547}
548
f0a10afc 549register_standard_option('spice-proxy', {
fb3a1b29 550 description => "SPICE proxy server. This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As reasonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).",
d07b7084 551 type => 'string', format => 'address',
9bbc4e17 552});
f0a10afc
DM
553
554register_standard_option('remote-viewer-config', {
555 description => "Returned values can be directly passed to the 'remote-viewer' application.",
556 additionalProperties => 1,
557 properties => {
558 type => { type => 'string' },
559 password => { type => 'string' },
560 proxy => { type => 'string' },
561 host => { type => 'string' },
562 'tls-port' => { type => 'integer' },
563 },
564});
565
c70c3bbc 566register_format('pve-startup-order', \&pve_verify_startup_order);
b0edd8e6
DM
567sub pve_verify_startup_order {
568 my ($value, $noerr) = @_;
569
570 return $value if pve_parse_startup_order($value);
571
572 return undef if $noerr;
573
574 die "unable to parse startup options\n";
575}
576
2d167ad0
WB
577my %bwlimit_opt = (
578 optional => 1,
579 type => 'number', minimum => '0',
580 format_description => 'LIMIT',
581);
582
583my $bwlimit_format = {
584 default => {
585 %bwlimit_opt,
34e75688 586 description => 'default bandwidth limit in KiB/s',
2d167ad0
WB
587 },
588 restore => {
589 %bwlimit_opt,
34e75688 590 description => 'bandwidth limit in KiB/s for restoring guests from backups',
2d167ad0
WB
591 },
592 migration => {
593 %bwlimit_opt,
34e75688 594 description => 'bandwidth limit in KiB/s for migrating guests (including moving local disks)',
2d167ad0
WB
595 },
596 clone => {
597 %bwlimit_opt,
34e75688 598 description => 'bandwidth limit in KiB/s for cloning disks',
2d167ad0
WB
599 },
600 move => {
601 %bwlimit_opt,
34e75688 602 description => 'bandwidth limit in KiB/s for moving disks',
2d167ad0
WB
603 },
604};
605register_format('bwlimit', $bwlimit_format);
606register_standard_option('bwlimit', {
607 description => "Set bandwidth/io limits various operations.",
608 optional => 1,
609 type => 'string',
610 format => $bwlimit_format,
611});
484b6b39
DC
612
613# used for pve-tag-list in e.g., guest configs
614register_format('pve-tag', \&pve_verify_tag);
615sub pve_verify_tag {
616 my ($value, $noerr) = @_;
617
618 return $value if $value =~ m/^[a-z0-9_][a-z0-9_\-\+\.]*$/i;
619
620 return undef if $noerr;
621
622 die "invalid characters in tag\n";
623}
2d167ad0 624
b0edd8e6
DM
625sub pve_parse_startup_order {
626 my ($value) = @_;
627
628 return undef if !$value;
629
630 my $res = {};
631
632 foreach my $p (split(/,/, $value)) {
633 next if $p =~ m/^\s*$/;
634
635 if ($p =~ m/^(order=)?(\d+)$/) {
636 $res->{order} = $2;
637 } elsif ($p =~ m/^up=(\d+)$/) {
638 $res->{up} = $1;
639 } elsif ($p =~ m/^down=(\d+)$/) {
640 $res->{down} = $1;
641 } else {
642 return undef;
643 }
644 }
645
31b5a3a7 646 return $res;
b0edd8e6
DM
647}
648
649PVE::JSONSchema::register_standard_option('pve-startup-order', {
650 description => "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.",
651 optional => 1,
652 type => 'string', format => 'pve-startup-order',
653 typetext => '[[order=]\d+] [,up=\d+] [,down=\d+] ',
654});
655
6e234325
WB
656register_format('pve-tfa-secret', \&pve_verify_tfa_secret);
657sub pve_verify_tfa_secret {
658 my ($key, $noerr) = @_;
659
660 # The old format used 16 base32 chars or 40 hex digits. Since they have a common subset it's
661 # hard to distinguish them without the our previous length constraints, so add a 'v2' of the
662 # format to support arbitrary lengths properly:
663 if ($key =~ /^v2-0x[0-9a-fA-F]{16,128}$/ || # hex
664 $key =~ /^v2-[A-Z2-7=]{16,128}$/ || # base32
665 $key =~ /^(?:[A-Z2-7=]{16}|[A-Fa-f0-9]{40})$/) # and the old pattern copy&pasted
666 {
667 return $key;
668 }
669
670 return undef if $noerr;
671
672 die "unable to decode TFA secret\n";
673}
674
e143e9d8 675sub check_format {
2f9e609a 676 my ($format, $value, $path) = @_;
e143e9d8 677
70fdc050
SR
678 if (ref($format) eq 'HASH') {
679 # hash ref cannot have validator/list/opt handling attached
680 return parse_property_string($format, $value, $path);
681 }
e143e9d8 682
70fdc050
SR
683 if (ref($format) eq 'CODE') {
684 # we are the (sole, old-style) validator
685 return $format->($value);
686 }
9bbc4e17 687
70fdc050
SR
688 return if $format eq 'regex';
689
690 my $parsed;
691 $format =~ m/^(.*?)(?:-a?(list|opt))?$/;
692 my ($format_name, $format_type) = ($1, $2 // 'none');
693 my $registered = get_format($format_name);
694 die "undefined format '$format'\n" if !$registered;
e143e9d8 695
70fdc050
SR
696 die "'-$format_type' format must have code ref, not hash\n"
697 if $format_type ne 'none' && ref($registered) ne 'CODE';
e143e9d8 698
70fdc050 699 if ($format_type eq 'list') {
e143e9d8
DM
700 # Note: we allow empty lists
701 foreach my $v (split_list($value)) {
70fdc050 702 $parsed = $registered->($v);
e143e9d8 703 }
70fdc050
SR
704 } elsif ($format_type eq 'opt') {
705 $parsed = $registered->($value) if $value;
e143e9d8 706 } else {
70fdc050
SR
707 if (ref($registered) eq 'HASH') {
708 # Note: this is the only case where a validator function could be
709 # attached, hence it's safe to handle that in parse_property_string.
710 # We do however have to call it with $format_name instead of
711 # $registered, so it knows about the name (and thus any validators).
712 $parsed = parse_property_string($format, $value, $path);
713 } else {
714 $parsed = $registered->($value);
715 }
e143e9d8 716 }
70fdc050
SR
717
718 return $parsed;
9bbc4e17 719}
e143e9d8 720
878fea8e
WB
721sub parse_size {
722 my ($value) = @_;
723
724 return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
725 my ($size, $unit) = ($1, $3);
726 if ($unit) {
727 if ($unit eq 'K') {
728 $size = $size * 1024;
729 } elsif ($unit eq 'M') {
730 $size = $size * 1024 * 1024;
731 } elsif ($unit eq 'G') {
732 $size = $size * 1024 * 1024 * 1024;
733 } elsif ($unit eq 'T') {
734 $size = $size * 1024 * 1024 * 1024 * 1024;
735 }
736 }
737 return int($size);
738};
739
740sub format_size {
741 my ($size) = @_;
742
743 $size = int($size);
744
745 my $kb = int($size/1024);
746 return $size if $kb*1024 != $size;
747
748 my $mb = int($kb/1024);
749 return "${kb}K" if $mb*1024 != $kb;
750
751 my $gb = int($mb/1024);
752 return "${mb}M" if $gb*1024 != $mb;
753
754 my $tb = int($gb/1024);
755 return "${gb}G" if $tb*1024 != $gb;
756
757 return "${tb}T";
758};
759
1b71e564
WB
760sub parse_boolean {
761 my ($bool) = @_;
762 return 1 if $bool =~ m/^(1|on|yes|true)$/i;
763 return 0 if $bool =~ m/^(0|off|no|false)$/i;
764 return undef;
765}
766
095b88fd 767sub parse_property_string {
d1e490c1
WB
768 my ($format, $data, $path, $additional_properties) = @_;
769
770 # In property strings we default to not allowing additional properties
771 $additional_properties = 0 if !defined($additional_properties);
095b88fd 772
7c1617b0 773 # Support named formats here, too:
70fdc050 774 my $validator;
7c1617b0 775 if (!ref($format)) {
70fdc050
SR
776 if (my $reg = get_format($format)) {
777 die "parse_property_string only accepts hash based named formats\n"
778 if ref($reg) ne 'HASH';
779
780 # named formats can have validators attached
781 $validator = $format_validators->{$format};
782
783 $format = $reg;
7c1617b0
WB
784 } else {
785 die "unknown format: $format\n";
786 }
787 } elsif (ref($format) ne 'HASH') {
788 die "unexpected format value of type ".ref($format)."\n";
789 }
790
095b88fd
WB
791 my $default_key;
792
793 my $res = {};
794 foreach my $part (split(/,/, $data)) {
795 next if $part =~ /^\s*$/;
796
797 if ($part =~ /^([^=]+)=(.+)$/) {
798 my ($k, $v) = ($1, $2);
2d468b1a 799 die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
095b88fd 800 my $schema = $format->{$k};
303a9b34 801 if (my $alias = $schema->{alias}) {
bf27456b
DM
802 if (my $key_alias = $schema->{keyAlias}) {
803 die "key alias '$key_alias' is already defined\n" if defined($res->{$key_alias});
804 $res->{$key_alias} = $k;
805 }
303a9b34
WB
806 $k = $alias;
807 $schema = $format->{$k};
808 }
bf27456b 809
2d468b1a 810 die "invalid key in comma-separated list property: $k\n" if !$schema;
095b88fd 811 if ($schema->{type} && $schema->{type} eq 'boolean') {
1b71e564 812 $v = parse_boolean($v) // $v;
095b88fd
WB
813 }
814 $res->{$k} = $v;
815 } elsif ($part !~ /=/) {
2d468b1a 816 die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
095b88fd
WB
817 foreach my $key (keys %$format) {
818 if ($format->{$key}->{default_key}) {
819 $default_key = $key;
820 if (!$res->{$default_key}) {
821 $res->{$default_key} = $part;
822 last;
823 }
2d468b1a 824 die "duplicate key in comma-separated list property: $default_key\n";
095b88fd
WB
825 }
826 }
f0ba41a1 827 die "value without key, but schema does not define a default key\n" if !$default_key;
095b88fd 828 } else {
2d468b1a 829 die "missing key in comma-separated list property\n";
095b88fd
WB
830 }
831 }
832
833 my $errors = {};
d1e490c1 834 check_object($path, $format, $res, $additional_properties, $errors);
095b88fd 835 if (scalar(%$errors)) {
2d468b1a 836 raise "format error\n", errors => $errors;
095b88fd
WB
837 }
838
70fdc050 839 return $validator->($res) if $validator;
095b88fd
WB
840 return $res;
841}
842
e143e9d8
DM
843sub add_error {
844 my ($errors, $path, $msg) = @_;
845
846 $path = '_root' if !$path;
9bbc4e17 847
e143e9d8
DM
848 if ($errors->{$path}) {
849 $errors->{$path} = join ('\n', $errors->{$path}, $msg);
850 } else {
851 $errors->{$path} = $msg;
852 }
853}
854
855sub is_number {
856 my $value = shift;
857
858 # see 'man perlretut'
9bbc4e17 859 return $value =~ /^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;
e143e9d8
DM
860}
861
862sub is_integer {
863 my $value = shift;
864
865 return $value =~ m/^[+-]?\d+$/;
866}
867
868sub check_type {
869 my ($path, $type, $value, $errors) = @_;
870
871 return 1 if !$type;
872
873 if (!defined($value)) {
874 return 1 if $type eq 'null';
9bbc4e17 875 die "internal error"
e143e9d8
DM
876 }
877
878 if (my $tt = ref($type)) {
879 if ($tt eq 'ARRAY') {
880 foreach my $t (@$type) {
881 my $tmperr = {};
882 check_type($path, $t, $value, $tmperr);
9bbc4e17 883 return 1 if !scalar(%$tmperr);
e143e9d8
DM
884 }
885 my $ttext = join ('|', @$type);
9bbc4e17 886 add_error($errors, $path, "type check ('$ttext') failed");
e143e9d8
DM
887 return undef;
888 } elsif ($tt eq 'HASH') {
889 my $tmperr = {};
890 check_prop($value, $type, $path, $tmperr);
9bbc4e17
TL
891 return 1 if !scalar(%$tmperr);
892 add_error($errors, $path, "type check failed");
e143e9d8
DM
893 return undef;
894 } else {
895 die "internal error - got reference type '$tt'";
896 }
897
898 } else {
899
900 return 1 if $type eq 'any';
901
902 if ($type eq 'null') {
903 if (defined($value)) {
904 add_error($errors, $path, "type check ('$type') failed - value is not null");
905 return undef;
906 }
907 return 1;
908 }
909
910 my $vt = ref($value);
911
912 if ($type eq 'array') {
913 if (!$vt || $vt ne 'ARRAY') {
914 add_error($errors, $path, "type check ('$type') failed");
915 return undef;
916 }
917 return 1;
918 } elsif ($type eq 'object') {
919 if (!$vt || $vt ne 'HASH') {
920 add_error($errors, $path, "type check ('$type') failed");
921 return undef;
922 }
923 return 1;
924 } elsif ($type eq 'coderef') {
925 if (!$vt || $vt ne 'CODE') {
926 add_error($errors, $path, "type check ('$type') failed");
927 return undef;
928 }
929 return 1;
88a490ff
WB
930 } elsif ($type eq 'string' && $vt eq 'Regexp') {
931 # qr// regexes can be used as strings and make sense for format=regex
932 return 1;
e143e9d8
DM
933 } else {
934 if ($vt) {
935 add_error($errors, $path, "type check ('$type') failed - got $vt");
936 return undef;
937 } else {
938 if ($type eq 'string') {
939 return 1; # nothing to check ?
940 } elsif ($type eq 'boolean') {
941 #if ($value =~ m/^(1|true|yes|on)$/i) {
942 if ($value eq '1') {
943 return 1;
944 #} elsif ($value =~ m/^(0|false|no|off)$/i) {
945 } elsif ($value eq '0') {
79501b2a 946 return 1; # return success (not value)
e143e9d8
DM
947 } else {
948 add_error($errors, $path, "type check ('$type') failed - got '$value'");
949 return undef;
950 }
951 } elsif ($type eq 'integer') {
952 if (!is_integer($value)) {
953 add_error($errors, $path, "type check ('$type') failed - got '$value'");
954 return undef;
955 }
956 return 1;
957 } elsif ($type eq 'number') {
958 if (!is_number($value)) {
959 add_error($errors, $path, "type check ('$type') failed - got '$value'");
960 return undef;
961 }
962 return 1;
963 } else {
964 return 1; # no need to verify unknown types
965 }
966 }
967 }
9bbc4e17 968 }
e143e9d8
DM
969
970 return undef;
971}
972
973sub check_object {
974 my ($path, $schema, $value, $additional_properties, $errors) = @_;
975
976 # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
977
978 my $st = ref($schema);
979 if (!$st || $st ne 'HASH') {
980 add_error($errors, $path, "Invalid schema definition.");
981 return;
982 }
983
984 my $vt = ref($value);
985 if (!$vt || $vt ne 'HASH') {
986 add_error($errors, $path, "an object is required");
987 return;
988 }
989
990 foreach my $k (keys %$schema) {
bf27456b 991 check_prop($value->{$k}, $schema->{$k}, $path ? "$path.$k" : $k, $errors);
e143e9d8
DM
992 }
993
994 foreach my $k (keys %$value) {
995
996 my $newpath = $path ? "$path.$k" : $k;
997
998 if (my $subschema = $schema->{$k}) {
999 if (my $requires = $subschema->{requires}) {
1000 if (ref($requires)) {
1001 #print "TEST: " . Dumper($value) . "\n", Dumper($requires) ;
1002 check_prop($value, $requires, $path, $errors);
1003 } elsif (!defined($value->{$requires})) {
9bbc4e17 1004 add_error($errors, $path ? "$path.$requires" : $requires,
8b6e737a 1005 "missing property - '$newpath' requires this property");
e143e9d8
DM
1006 }
1007 }
1008
1009 next; # value is already checked above
1010 }
1011
1012 if (defined ($additional_properties) && !$additional_properties) {
1013 add_error($errors, $newpath, "property is not defined in schema " .
1014 "and the schema does not allow additional properties");
1015 next;
1016 }
1017 check_prop($value->{$k}, $additional_properties, $newpath, $errors)
1018 if ref($additional_properties);
1019 }
1020}
1021
86425a09
WB
1022sub check_object_warn {
1023 my ($path, $schema, $value, $additional_properties) = @_;
1024 my $errors = {};
1025 check_object($path, $schema, $value, $additional_properties, $errors);
1026 if (scalar(%$errors)) {
1027 foreach my $k (keys %$errors) {
1028 warn "parse error: $k: $errors->{$k}\n";
1029 }
1030 return 0;
1031 }
1032 return 1;
1033}
1034
e143e9d8
DM
1035sub check_prop {
1036 my ($value, $schema, $path, $errors) = @_;
1037
1038 die "internal error - no schema" if !$schema;
1039 die "internal error" if !$errors;
1040
1041 #print "check_prop $path\n" if $value;
1042
1043 my $st = ref($schema);
1044 if (!$st || $st ne 'HASH') {
1045 add_error($errors, $path, "Invalid schema definition.");
1046 return;
1047 }
1048
1049 # if it extends another schema, it must pass that schema as well
1050 if($schema->{extends}) {
1051 check_prop($value, $schema->{extends}, $path, $errors);
1052 }
1053
1054 if (!defined ($value)) {
1055 return if $schema->{type} && $schema->{type} eq 'null';
445e8267 1056 if (!$schema->{optional} && !$schema->{alias} && !$schema->{group}) {
e143e9d8
DM
1057 add_error($errors, $path, "property is missing and it is not optional");
1058 }
1059 return;
1060 }
1061
1062 return if !check_type($path, $schema->{type}, $value, $errors);
1063
1064 if ($schema->{disallow}) {
1065 my $tmperr = {};
1066 if (check_type($path, $schema->{disallow}, $value, $tmperr)) {
1067 add_error($errors, $path, "disallowed value was matched");
1068 return;
1069 }
1070 }
1071
1072 if (my $vt = ref($value)) {
1073
1074 if ($vt eq 'ARRAY') {
1075 if ($schema->{items}) {
1076 my $it = ref($schema->{items});
1077 if ($it && $it eq 'ARRAY') {
1078 #die "implement me $path: $vt " . Dumper($schema) ."\n". Dumper($value);
1079 die "not implemented";
1080 } else {
1081 my $ind = 0;
1082 foreach my $el (@$value) {
1083 check_prop($el, $schema->{items}, "${path}[$ind]", $errors);
1084 $ind++;
1085 }
1086 }
1087 }
9bbc4e17 1088 return;
e143e9d8
DM
1089 } elsif ($schema->{properties} || $schema->{additionalProperties}) {
1090 check_object($path, defined($schema->{properties}) ? $schema->{properties} : {},
1091 $value, $schema->{additionalProperties}, $errors);
1092 return;
1093 }
1094
1095 } else {
1096
1097 if (my $format = $schema->{format}) {
2f9e609a 1098 eval { check_format($format, $value, $path); };
e143e9d8
DM
1099 if ($@) {
1100 add_error($errors, $path, "invalid format - $@");
1101 return;
1102 }
1103 }
1104
1105 if (my $pattern = $schema->{pattern}) {
1106 if ($value !~ m/^$pattern$/) {
1107 add_error($errors, $path, "value does not match the regex pattern");
1108 return;
1109 }
1110 }
1111
1112 if (defined (my $max = $schema->{maxLength})) {
1113 if (length($value) > $max) {
1114 add_error($errors, $path, "value may only be $max characters long");
1115 return;
1116 }
1117 }
1118
1119 if (defined (my $min = $schema->{minLength})) {
1120 if (length($value) < $min) {
1121 add_error($errors, $path, "value must be at least $min characters long");
1122 return;
1123 }
1124 }
9bbc4e17 1125
e143e9d8
DM
1126 if (is_number($value)) {
1127 if (defined (my $max = $schema->{maximum})) {
9bbc4e17 1128 if ($value > $max) {
e143e9d8
DM
1129 add_error($errors, $path, "value must have a maximum value of $max");
1130 return;
1131 }
1132 }
1133
1134 if (defined (my $min = $schema->{minimum})) {
9bbc4e17 1135 if ($value < $min) {
e143e9d8
DM
1136 add_error($errors, $path, "value must have a minimum value of $min");
1137 return;
1138 }
1139 }
1140 }
1141
1142 if (my $ea = $schema->{enum}) {
1143
1144 my $found;
1145 foreach my $ev (@$ea) {
1146 if ($ev eq $value) {
1147 $found = 1;
1148 last;
1149 }
1150 }
1151 if (!$found) {
1152 add_error($errors, $path, "value '$value' does not have a value in the enumeration '" .
1153 join(", ", @$ea) . "'");
1154 }
1155 }
1156 }
1157}
1158
1159sub validate {
1160 my ($instance, $schema, $errmsg) = @_;
1161
1162 my $errors = {};
1163 $errmsg = "Parameter verification failed.\n" if !$errmsg;
1164
1165 # todo: cycle detection is only needed for debugging, I guess
1166 # we can disable that in the final release
1167 # todo: is there a better/faster way to detect cycles?
1168 my $cycles = 0;
1169 find_cycle($instance, sub { $cycles = 1 });
1170 if ($cycles) {
1171 add_error($errors, undef, "data structure contains recursive cycles");
1172 } elsif ($schema) {
1173 check_prop($instance, $schema, '', $errors);
1174 }
9bbc4e17 1175
e143e9d8
DM
1176 if (scalar(%$errors)) {
1177 raise $errmsg, code => HTTP_BAD_REQUEST, errors => $errors;
1178 }
1179
1180 return 1;
1181}
1182
1183my $schema_valid_types = ["string", "object", "coderef", "array", "boolean", "number", "integer", "null", "any"];
1184my $default_schema_noref = {
1185 description => "This is the JSON Schema for JSON Schemas.",
1186 type => [ "object" ],
1187 additionalProperties => 0,
1188 properties => {
1189 type => {
1190 type => ["string", "array"],
1191 description => "This is a type definition value. This can be a simple type, or a union type",
1192 optional => 1,
1193 default => "any",
1194 items => {
1195 type => "string",
1196 enum => $schema_valid_types,
1197 },
1198 enum => $schema_valid_types,
1199 },
1200 optional => {
1201 type => "boolean",
1202 description => "This indicates that the instance property in the instance object is not required.",
1203 optional => 1,
1204 default => 0
1205 },
1206 properties => {
1207 type => "object",
1208 description => "This is a definition for the properties of an object value",
1209 optional => 1,
1210 default => {},
1211 },
1212 items => {
1213 type => "object",
1214 description => "When the value is an array, this indicates the schema to use to validate each item in an array",
1215 optional => 1,
1216 default => {},
1217 },
1218 additionalProperties => {
1219 type => [ "boolean", "object"],
1220 description => "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
1221 optional => 1,
1222 default => {},
1223 },
1224 minimum => {
1225 type => "number",
1226 optional => 1,
1227 description => "This indicates the minimum value for the instance property when the type of the instance value is a number.",
1228 },
1229 maximum => {
1230 type => "number",
1231 optional => 1,
1232 description => "This indicates the maximum value for the instance property when the type of the instance value is a number.",
1233 },
1234 minLength => {
1235 type => "integer",
1236 description => "When the instance value is a string, this indicates minimum length of the string",
1237 optional => 1,
1238 minimum => 0,
1239 default => 0,
9bbc4e17 1240 },
e143e9d8
DM
1241 maxLength => {
1242 type => "integer",
1243 description => "When the instance value is a string, this indicates maximum length of the string.",
1244 optional => 1,
1245 },
1246 typetext => {
1247 type => "string",
1248 optional => 1,
1249 description => "A text representation of the type (used to generate documentation).",
1250 },
1251 pattern => {
1252 type => "string",
1253 format => "regex",
166e27c7 1254 description => "When the instance value is a string, this provides a regular expression that a instance string value should match in order to be valid.",
e143e9d8
DM
1255 optional => 1,
1256 default => ".*",
166e27c7 1257 },
e143e9d8
DM
1258 enum => {
1259 type => "array",
1260 optional => 1,
1261 description => "This provides an enumeration of possible values that are valid for the instance property.",
1262 },
1263 description => {
1264 type => "string",
1265 optional => 1,
1266 description => "This provides a description of the purpose the instance property. The value can be a string or it can be an object with properties corresponding to various different instance languages (with an optional default property indicating the default description).",
1267 },
32f8e0c7
DM
1268 verbose_description => {
1269 type => "string",
1270 optional => 1,
1271 description => "This provides a more verbose description.",
1272 },
d5d10f85
WB
1273 format_description => {
1274 type => "string",
1275 optional => 1,
1276 description => "This provides a shorter (usually just one word) description for a property used to generate descriptions for comma separated list property strings.",
1277 },
166e27c7
WB
1278 title => {
1279 type => "string",
e143e9d8 1280 optional => 1,
166e27c7
WB
1281 description => "This provides the title of the property",
1282 },
03c1e2a0
DM
1283 renderer => {
1284 type => "string",
1285 optional => 1,
1286 description => "This is used to provide rendering hints to format cli command output.",
1287 },
166e27c7
WB
1288 requires => {
1289 type => [ "string", "object" ],
e143e9d8 1290 optional => 1,
166e27c7
WB
1291 description => "indicates a required property or a schema that must be validated if this property is present",
1292 },
1293 format => {
2f9e609a 1294 type => [ "string", "object" ],
e143e9d8 1295 optional => 1,
166e27c7
WB
1296 description => "This indicates what format the data is among some predefined formats which may include:\n\ndate - a string following the ISO format \naddress \nschema - a schema definition object \nperson \npage \nhtml - a string representing HTML",
1297 },
095b88fd
WB
1298 default_key => {
1299 type => "boolean",
1300 optional => 1,
1301 description => "Whether this is the default key in a comma separated list property string.",
1302 },
303a9b34
WB
1303 alias => {
1304 type => 'string',
1305 optional => 1,
1306 description => "When a key represents the same property as another it can be an alias to it, causing the parsed datastructure to use the other key to store the current value under.",
1307 },
bf27456b 1308 keyAlias => {
445e8267
WB
1309 type => 'string',
1310 optional => 1,
bf27456b
DM
1311 description => "Allows to store the current 'key' as value of another property. Only valid if used together with 'alias'.",
1312 requires => 'alias',
445e8267 1313 },
e143e9d8
DM
1314 default => {
1315 type => "any",
1316 optional => 1,
1317 description => "This indicates the default for the instance property."
1318 },
166e27c7 1319 completion => {
7829989f
DM
1320 type => 'coderef',
1321 description => "Bash completion function. This function should return a list of possible values.",
1322 optional => 1,
166e27c7
WB
1323 },
1324 disallow => {
1325 type => "object",
e143e9d8 1326 optional => 1,
166e27c7 1327 description => "This attribute may take the same values as the \"type\" attribute, however if the instance matches the type or if this value is an array and the instance matches any type or schema in the array, then this instance is not valid.",
e143e9d8 1328 },
166e27c7
WB
1329 extends => {
1330 type => "object",
e143e9d8 1331 optional => 1,
166e27c7 1332 description => "This indicates the schema extends the given schema. All instances of this schema must be valid to by the extended schema also.",
e143e9d8 1333 default => {},
166e27c7
WB
1334 },
1335 # this is from hyper schema
1336 links => {
1337 type => "array",
1338 description => "This defines the link relations of the instance objects",
1339 optional => 1,
e143e9d8 1340 items => {
166e27c7
WB
1341 type => "object",
1342 properties => {
1343 href => {
1344 type => "string",
1345 description => "This defines the target URL for the relation and can be parameterized using {propertyName} notation. It should be resolved as a URI-reference relative to the URI that was used to retrieve the instance document",
1346 },
1347 rel => {
1348 type => "string",
1349 description => "This is the name of the link relation",
1350 optional => 1,
1351 default => "full",
1352 },
e143e9d8 1353 method => {
166e27c7
WB
1354 type => "string",
1355 description => "For submission links, this defines the method that should be used to access the target resource",
1356 optional => 1,
1357 default => "GET",
e143e9d8
DM
1358 },
1359 },
1360 },
1361 },
f8d4eff9
SI
1362 print_width => {
1363 type => "integer",
1364 description => "For CLI context, this defines the maximal width to print before truncating",
1365 optional => 1,
1366 },
9bbc4e17 1367 }
e143e9d8
DM
1368};
1369
1370my $default_schema = Storable::dclone($default_schema_noref);
1371
1372$default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
1373$default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
1374
1375$default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
1376$default_schema->{properties}->{items}->{additionalProperties} = 0;
1377
1378$default_schema->{properties}->{disallow}->{properties} = $default_schema->{properties};
1379$default_schema->{properties}->{disallow}->{additionalProperties} = 0;
1380
1381$default_schema->{properties}->{requires}->{properties} = $default_schema->{properties};
1382$default_schema->{properties}->{requires}->{additionalProperties} = 0;
1383
1384$default_schema->{properties}->{extends}->{properties} = $default_schema->{properties};
1385$default_schema->{properties}->{extends}->{additionalProperties} = 0;
1386
1387my $method_schema = {
1388 type => "object",
1389 additionalProperties => 0,
1390 properties => {
1391 description => {
1392 description => "This a description of the method",
1393 optional => 1,
1394 },
1395 name => {
1396 type => 'string',
1397 description => "This indicates the name of the function to call.",
1398 optional => 1,
1399 requires => {
1400 additionalProperties => 1,
1401 properties => {
1402 name => {},
1403 description => {},
1404 code => {},
1405 method => {},
1406 parameters => {},
1407 path => {},
1408 parameters => {},
1409 returns => {},
9bbc4e17 1410 }
e143e9d8
DM
1411 },
1412 },
1413 method => {
1414 type => 'string',
1415 description => "The HTTP method name.",
1416 enum => [ 'GET', 'POST', 'PUT', 'DELETE' ],
1417 optional => 1,
1418 },
1419 protected => {
1420 type => 'boolean',
9bbc4e17 1421 description => "Method needs special privileges - only pvedaemon can execute it",
e143e9d8
DM
1422 optional => 1,
1423 },
4c72ade0
FG
1424 allowtoken => {
1425 type => 'boolean',
1426 description => "Method is available for clients authenticated using an API token.",
1427 optional => 1,
1428 default => 1,
1429 },
62a8f27b
DM
1430 download => {
1431 type => 'boolean',
1432 description => "Method downloads the file content (filename is the return value of the method).",
1433 optional => 1,
1434 },
e143e9d8
DM
1435 proxyto => {
1436 type => 'string',
1437 description => "A parameter name. If specified, all calls to this method are proxied to the host contained in that parameter.",
1438 optional => 1,
1439 },
031efdd0
DM
1440 proxyto_callback => {
1441 type => 'coderef',
fb3a1b29 1442 description => "A function which is called to resolve the proxyto attribute. The default implementation returns the value of the 'proxyto' parameter.",
031efdd0
DM
1443 optional => 1,
1444 },
e143e9d8
DM
1445 permissions => {
1446 type => 'object',
1447 description => "Required access permissions. By default only 'root' is allowed to access this method.",
1448 optional => 1,
1449 additionalProperties => 0,
1450 properties => {
b18d1722
DM
1451 description => {
1452 description => "Describe access permissions.",
1453 optional => 1,
1454 },
e143e9d8 1455 user => {
9bbc4e17
TL
1456 description => "A simply way to allow access for 'all' authenticated users. Value 'world' is used to allow access without credentials.",
1457 type => 'string',
b18d1722 1458 enum => ['all', 'world'],
e143e9d8
DM
1459 optional => 1,
1460 },
b18d1722
DM
1461 check => {
1462 description => "Array of permission checks (prefix notation).",
9bbc4e17
TL
1463 type => 'array',
1464 optional => 1
b18d1722 1465 },
e143e9d8
DM
1466 },
1467 },
1468 match_name => {
1469 description => "Used internally",
1470 optional => 1,
1471 },
1472 match_re => {
1473 description => "Used internally",
1474 optional => 1,
1475 },
1476 path => {
1477 type => 'string',
1478 description => "path for URL matching (uri template)",
1479 },
1480 fragmentDelimiter => {
1481 type => 'string',
fb3a1b29 1482 description => "A way to override the default fragment delimiter '/'. This only works on a whole sub-class. You can set this to the empty string to match the whole rest of the URI.",
e143e9d8
DM
1483 optional => 1,
1484 },
1485 parameters => {
1486 type => 'object',
1487 description => "JSON Schema for parameters.",
1488 optional => 1,
1489 },
1490 returns => {
1491 type => 'object',
1492 description => "JSON Schema for return value.",
1493 optional => 1,
1494 },
1495 code => {
1496 type => 'coderef',
fb3a1b29 1497 description => "method implementation (code reference)",
e143e9d8
DM
1498 optional => 1,
1499 },
1500 subclass => {
1501 type => 'string',
1502 description => "Delegate call to this class (perl class string).",
1503 optional => 1,
1504 requires => {
1505 additionalProperties => 0,
1506 properties => {
1507 subclass => {},
1508 path => {},
1509 match_name => {},
1510 match_re => {},
1511 fragmentDelimiter => { optional => 1 }
9bbc4e17 1512 }
e143e9d8 1513 },
9bbc4e17 1514 },
e143e9d8
DM
1515 },
1516
1517};
1518
1519sub validate_schema {
9bbc4e17 1520 my ($schema) = @_;
e143e9d8
DM
1521
1522 my $errmsg = "internal error - unable to verify schema\n";
1523 validate($schema, $default_schema, $errmsg);
1524}
1525
1526sub validate_method_info {
1527 my $info = shift;
1528
1529 my $errmsg = "internal error - unable to verify method info\n";
1530 validate($info, $method_schema, $errmsg);
9bbc4e17 1531
e143e9d8
DM
1532 validate_schema($info->{parameters}) if $info->{parameters};
1533 validate_schema($info->{returns}) if $info->{returns};
1534}
1535
1536# run a self test on load
9bbc4e17 1537# make sure we can verify the default schema
e143e9d8
DM
1538validate_schema($default_schema_noref);
1539validate_schema($method_schema);
1540
1541# and now some utility methods (used by pve api)
1542sub method_get_child_link {
1543 my ($info) = @_;
1544
1545 return undef if !$info;
1546
1547 my $schema = $info->{returns};
1548 return undef if !$schema || !$schema->{type} || $schema->{type} ne 'array';
1549
1550 my $links = $schema->{links};
1551 return undef if !$links;
1552
1553 my $found;
1554 foreach my $lnk (@$links) {
1555 if ($lnk->{href} && $lnk->{rel} && ($lnk->{rel} eq 'child')) {
1556 $found = $lnk;
1557 last;
1558 }
1559 }
1560
1561 return $found;
1562}
1563
9bbc4e17 1564# a way to parse command line parameters, using a
e143e9d8
DM
1565# schema to configure Getopt::Long
1566sub get_options {
4842b651 1567 my ($schema, $args, $arg_param, $fixed_param, $param_mapping_hash) = @_;
e143e9d8
DM
1568
1569 if (!$schema || !$schema->{properties}) {
1570 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1571 if scalar(@$args) != 0;
1572 return {};
1573 }
1574
0ce82909
DM
1575 my $list_param;
1576 if ($arg_param && !ref($arg_param)) {
1577 my $pd = $schema->{properties}->{$arg_param};
1578 die "expected list format $pd->{format}"
1579 if !($pd && $pd->{format} && $pd->{format} =~ m/-list/);
1580 $list_param = $arg_param;
1581 }
1582
c7171ff2 1583 my @interactive = ();
e143e9d8
DM
1584 my @getopt = ();
1585 foreach my $prop (keys %{$schema->{properties}}) {
1586 my $pd = $schema->{properties}->{$prop};
aab47b58 1587 next if $list_param && $prop eq $list_param;
0ce82909 1588 next if defined($fixed_param->{$prop});
e143e9d8 1589
c7171ff2
WB
1590 my $mapping = $param_mapping_hash->{$prop};
1591 if ($mapping && $mapping->{interactive}) {
1592 # interactive parameters such as passwords: make the argument
1593 # optional and call the mapping function afterwards.
1594 push @getopt, "$prop:s";
1595 push @interactive, [$prop, $mapping->{func}];
e143e9d8
DM
1596 } elsif ($pd->{type} eq 'boolean') {
1597 push @getopt, "$prop:s";
1598 } else {
23dc9401 1599 if ($pd->{format} && $pd->{format} =~ m/-a?list/) {
8ba7c72b
DM
1600 push @getopt, "$prop=s@";
1601 } else {
1602 push @getopt, "$prop=s";
1603 }
e143e9d8
DM
1604 }
1605 }
1606
1068aeb3
WB
1607 Getopt::Long::Configure('prefix_pattern=(--|-)');
1608
e143e9d8
DM
1609 my $opts = {};
1610 raise("unable to parse option\n", code => HTTP_BAD_REQUEST)
1611 if !Getopt::Long::GetOptionsFromArray($args, $opts, @getopt);
1d21344c 1612
5851be88 1613 if (@$args) {
0ce82909
DM
1614 if ($list_param) {
1615 $opts->{$list_param} = $args;
1616 $args = [];
1617 } elsif (ref($arg_param)) {
804bc621
TL
1618 for (my $i = 0; $i < scalar(@$arg_param); $i++) {
1619 my $arg_name = $arg_param->[$i];
5851be88
WB
1620 if ($opts->{'extra-args'}) {
1621 raise("internal error: extra-args must be the last argument\n", code => HTTP_BAD_REQUEST);
1622 }
1623 if ($arg_name eq 'extra-args') {
1624 $opts->{'extra-args'} = $args;
1625 $args = [];
1626 next;
1627 }
804bc621
TL
1628 if (!@$args) {
1629 # check if all left-over arg_param are optional, else we
1630 # must die as the mapping is then ambigious
1631 for (my $j = $i; $j < scalar(@$arg_param); $j++) {
1632 my $prop = $arg_param->[$j];
1633 raise("not enough arguments\n", code => HTTP_BAD_REQUEST)
1634 if !$schema->{properties}->{$prop}->{optional};
1635 }
1636 }
5851be88 1637 $opts->{$arg_name} = shift @$args;
0ce82909 1638 }
5851be88 1639 raise("too many arguments\n", code => HTTP_BAD_REQUEST) if @$args;
0ce82909
DM
1640 } else {
1641 raise("too many arguments\n", code => HTTP_BAD_REQUEST)
1642 if scalar(@$args) != 0;
1643 }
ff2bf45f
DM
1644 } else {
1645 if (ref($arg_param)) {
1646 foreach my $arg_name (@$arg_param) {
1647 if ($arg_name eq 'extra-args') {
1648 $opts->{'extra-args'} = [];
3fe29ce6 1649 } elsif (!$schema->{properties}->{$arg_name}->{optional}) {
ff2bf45f
DM
1650 raise("not enough arguments\n", code => HTTP_BAD_REQUEST);
1651 }
1652 }
1653 }
1d21344c
DM
1654 }
1655
c7171ff2
WB
1656 foreach my $entry (@interactive) {
1657 my ($opt, $func) = @$entry;
1658 my $pd = $schema->{properties}->{$opt};
1659 my $value = $opts->{$opt};
1660 if (defined($value) || !$pd->{optional}) {
1661 $opts->{$opt} = $func->($value);
1662 }
1663 }
1664
c9902568 1665 # decode after Getopt as we are not sure how well it handles unicode
24197a9f 1666 foreach my $p (keys %$opts) {
c9902568
TL
1667 if (!ref($opts->{$p})) {
1668 $opts->{$p} = decode('locale', $opts->{$p});
1669 } elsif (ref($opts->{$p}) eq 'ARRAY') {
1670 my $tmp = [];
1671 foreach my $v (@{$opts->{$p}}) {
1672 push @$tmp, decode('locale', $v);
1673 }
1674 $opts->{$p} = $tmp;
1675 } elsif (ref($opts->{$p}) eq 'SCALAR') {
1676 $opts->{$p} = decode('locale', $$opts->{$p});
1677 } else {
1678 raise("decoding options failed, unknown reference\n", code => HTTP_BAD_REQUEST);
1679 }
24197a9f 1680 }
815b2aba 1681
e143e9d8
DM
1682 foreach my $p (keys %$opts) {
1683 if (my $pd = $schema->{properties}->{$p}) {
1684 if ($pd->{type} eq 'boolean') {
1685 if ($opts->{$p} eq '') {
1686 $opts->{$p} = 1;
1b71e564
WB
1687 } elsif (defined(my $bool = parse_boolean($opts->{$p}))) {
1688 $opts->{$p} = $bool;
e143e9d8
DM
1689 } else {
1690 raise("unable to parse boolean option\n", code => HTTP_BAD_REQUEST);
1691 }
23dc9401 1692 } elsif ($pd->{format}) {
8ba7c72b 1693
23dc9401 1694 if ($pd->{format} =~ m/-list/) {
8ba7c72b 1695 # allow --vmid 100 --vmid 101 and --vmid 100,101
23dc9401 1696 # allow --dow mon --dow fri and --dow mon,fri
43479146 1697 $opts->{$p} = join(",", @{$opts->{$p}}) if ref($opts->{$p}) eq 'ARRAY';
23dc9401 1698 } elsif ($pd->{format} =~ m/-alist/) {
8ba7c72b
DM
1699 # we encode array as \0 separated strings
1700 # Note: CGI.pm also use this encoding
1701 if (scalar(@{$opts->{$p}}) != 1) {
1702 $opts->{$p} = join("\0", @{$opts->{$p}});
1703 } else {
1704 # st that split_list knows it is \0 terminated
1705 my $v = $opts->{$p}->[0];
1706 $opts->{$p} = "$v\0";
1707 }
1708 }
e143e9d8 1709 }
9bbc4e17 1710 }
e143e9d8
DM
1711 }
1712
0ce82909
DM
1713 foreach my $p (keys %$fixed_param) {
1714 $opts->{$p} = $fixed_param->{$p};
e143e9d8
DM
1715 }
1716
1717 return $opts;
1718}
1719
1720# A way to parse configuration data by giving a json schema
1721sub parse_config {
1722 my ($schema, $filename, $raw) = @_;
1723
1724 # do fast check (avoid validate_schema($schema))
9bbc4e17 1725 die "got strange schema" if !$schema->{type} ||
e143e9d8
DM
1726 !$schema->{properties} || $schema->{type} ne 'object';
1727
1728 my $cfg = {};
1729
3c4d612a 1730 while ($raw =~ /^\s*(.+?)\s*$/gm) {
e143e9d8 1731 my $line = $1;
e143e9d8 1732
3c4d612a
WB
1733 next if $line =~ /^#/;
1734
1735 if ($line =~ m/^(\S+?):\s*(.*)$/) {
e143e9d8
DM
1736 my $key = $1;
1737 my $value = $2;
9bbc4e17 1738 if ($schema->{properties}->{$key} &&
e143e9d8
DM
1739 $schema->{properties}->{$key}->{type} eq 'boolean') {
1740
1b71e564 1741 $value = parse_boolean($value) // $value;
e143e9d8
DM
1742 }
1743 $cfg->{$key} = $value;
1744 } else {
1745 warn "ignore config line: $line\n"
1746 }
1747 }
1748
1749 my $errors = {};
1750 check_prop($cfg, $schema, '', $errors);
1751
1752 foreach my $k (keys %$errors) {
1753 warn "parse error in '$filename' - '$k': $errors->{$k}\n";
1754 delete $cfg->{$k};
9bbc4e17 1755 }
e143e9d8
DM
1756
1757 return $cfg;
1758}
1759
1760# generate simple key/value file
1761sub dump_config {
1762 my ($schema, $filename, $cfg) = @_;
1763
1764 # do fast check (avoid validate_schema($schema))
9bbc4e17 1765 die "got strange schema" if !$schema->{type} ||
e143e9d8
DM
1766 !$schema->{properties} || $schema->{type} ne 'object';
1767
1768 validate($cfg, $schema, "validation error in '$filename'\n");
1769
1770 my $data = '';
1771
821d408d 1772 foreach my $k (sort keys %$cfg) {
e143e9d8
DM
1773 $data .= "$k: $cfg->{$k}\n";
1774 }
1775
1776 return $data;
1777}
1778
bf27456b
DM
1779# helpers used to generate our manual pages
1780
1781my $find_schema_default_key = sub {
1782 my ($format) = @_;
1783
1784 my $default_key;
1785 my $keyAliasProps = {};
1786
1787 foreach my $key (keys %$format) {
1788 my $phash = $format->{$key};
1789 if ($phash->{default_key}) {
1790 die "multiple default keys in schema ($default_key, $key)\n"
1791 if defined($default_key);
1792 die "default key '$key' is an alias - this is not allowed\n"
1793 if defined($phash->{alias});
1794 die "default key '$key' with keyAlias attribute is not allowed\n"
1795 if $phash->{keyAlias};
bf27456b
DM
1796 $default_key = $key;
1797 }
1798 my $key_alias = $phash->{keyAlias};
c88c582d
DM
1799 die "found keyAlias without 'alias definition for '$key'\n"
1800 if $key_alias && !$phash->{alias};
1801
bf27456b
DM
1802 if ($phash->{alias} && $key_alias) {
1803 die "inconsistent keyAlias '$key_alias' definition"
1804 if defined($keyAliasProps->{$key_alias}) &&
1805 $keyAliasProps->{$key_alias} ne $phash->{alias};
1806 $keyAliasProps->{$key_alias} = $phash->{alias};
1807 }
1808 }
1809
1810 return wantarray ? ($default_key, $keyAliasProps) : $default_key;
1811};
1812
1813sub generate_typetext {
abc1afd8 1814 my ($format, $list_enums) = @_;
bf27456b 1815
d8c2b947 1816 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
bf27456b
DM
1817
1818 my $res = '';
1819 my $add_sep = 0;
1820
1821 my $add_option_string = sub {
1822 my ($text, $optional) = @_;
1823
1824 if ($add_sep) {
1825 $text = ",$text";
1826 $res .= ' ';
1827 }
1828 $text = "[$text]" if $optional;
1829 $res .= $text;
1830 $add_sep = 1;
1831 };
1832
1833 my $format_key_value = sub {
1834 my ($key, $phash) = @_;
1835
1836 die "internal error" if defined($phash->{alias});
1837
1838 my $keytext = $key;
1839
1840 my $typetext = '';
1841
1842 if (my $desc = $phash->{format_description}) {
1843 $typetext .= "<$desc>";
1844 } elsif (my $text = $phash->{typetext}) {
1845 $typetext .= $text;
1846 } elsif (my $enum = $phash->{enum}) {
abc1afd8
DM
1847 if ($list_enums || (scalar(@$enum) <= 3)) {
1848 $typetext .= '<' . join('|', @$enum) . '>';
1849 } else {
1850 $typetext .= '<enum>';
1851 }
bf27456b
DM
1852 } elsif ($phash->{type} eq 'boolean') {
1853 $typetext .= '<1|0>';
1854 } elsif ($phash->{type} eq 'integer') {
1855 $typetext .= '<integer>';
1856 } elsif ($phash->{type} eq 'number') {
1857 $typetext .= '<number>';
1858 } else {
1859 die "internal error: neither format_description nor typetext found for option '$key'";
1860 }
1861
1862 if (defined($default_key) && ($default_key eq $key)) {
1863 &$add_option_string("[$keytext=]$typetext", $phash->{optional});
1864 } else {
1865 &$add_option_string("$keytext=$typetext", $phash->{optional});
1866 }
1867 };
1868
d8c2b947 1869 my $done = {};
bf27456b 1870
d8c2b947
DM
1871 my $cond_add_key = sub {
1872 my ($key) = @_;
1873
1874 return if $done->{$key}; # avoid duplicates
1875
1876 $done->{$key} = 1;
bf27456b
DM
1877
1878 my $phash = $format->{$key};
1879
d8c2b947
DM
1880 return if !$phash; # should not happen
1881
1882 return if $phash->{alias};
bf27456b
DM
1883
1884 &$format_key_value($key, $phash);
1885
d8c2b947
DM
1886 };
1887
1888 &$cond_add_key($default_key) if defined($default_key);
1889
1890 # add required keys first
1891 foreach my $key (sort keys %$format) {
1892 my $phash = $format->{$key};
1893 &$cond_add_key($key) if $phash && !$phash->{optional};
1894 }
1895
1896 # add the rest
1897 foreach my $key (sort keys %$format) {
1898 &$cond_add_key($key);
1899 }
1900
1901 foreach my $keyAlias (sort keys %$keyAliasProps) {
1902 &$add_option_string("<$keyAlias>=<$keyAliasProps->{$keyAlias }>", 1);
bf27456b
DM
1903 }
1904
1905 return $res;
1906}
1907
1908sub print_property_string {
1909 my ($data, $format, $skip, $path) = @_;
1910
d500c038 1911 my $validator;
bf27456b
DM
1912 if (ref($format) ne 'HASH') {
1913 my $schema = get_format($format);
1914 die "not a valid format: $format\n" if !$schema;
d500c038
SR
1915 # named formats can have validators attached
1916 $validator = $format_validators->{$format};
bf27456b
DM
1917 $format = $schema;
1918 }
1919
1920 my $errors = {};
1921 check_object($path, $format, $data, undef, $errors);
1922 if (scalar(%$errors)) {
1923 raise "format error", errors => $errors;
1924 }
1925
d500c038
SR
1926 $data = $validator->($data) if $validator;
1927
bf27456b
DM
1928 my ($default_key, $keyAliasProps) = &$find_schema_default_key($format);
1929
1930 my $res = '';
1931 my $add_sep = 0;
1932
1933 my $add_option_string = sub {
1934 my ($text) = @_;
1935
1936 $res .= ',' if $add_sep;
1937 $res .= $text;
1938 $add_sep = 1;
1939 };
1940
1941 my $format_value = sub {
1942 my ($key, $value, $format) = @_;
1943
1944 if (defined($format) && ($format eq 'disk-size')) {
1945 return format_size($value);
1946 } else {
1947 die "illegal value with commas for $key\n" if $value =~ /,/;
1948 return $value;
1949 }
1950 };
1951
2289890b 1952 my $done = { map { $_ => 1 } @$skip };
bf27456b
DM
1953
1954 my $cond_add_key = sub {
971353e8 1955 my ($key, $isdefault) = @_;
bf27456b
DM
1956
1957 return if $done->{$key}; # avoid duplicates
1958
1959 $done->{$key} = 1;
1960
1961 my $value = $data->{$key};
1962
1963 return if !defined($value);
1964
1965 my $phash = $format->{$key};
1966
1967 # try to combine values if we have key aliases
1968 if (my $combine = $keyAliasProps->{$key}) {
1969 if (defined(my $combine_value = $data->{$combine})) {
1970 my $combine_format = $format->{$combine}->{format};
1971 my $value_str = &$format_value($key, $value, $phash->{format});
1972 my $combine_str = &$format_value($combine, $combine_value, $combine_format);
1973 &$add_option_string("${value_str}=${combine_str}");
1974 $done->{$combine} = 1;
1975 return;
1976 }
1977 }
1978
1979 if ($phash && $phash->{alias}) {
1980 $phash = $format->{$phash->{alias}};
1981 }
1982
1983 die "invalid key '$key'\n" if !$phash;
1984 die "internal error" if defined($phash->{alias});
1985
1986 my $value_str = &$format_value($key, $value, $phash->{format});
971353e8
WB
1987 if ($isdefault) {
1988 &$add_option_string($value_str);
1989 } else {
1990 &$add_option_string("$key=${value_str}");
1991 }
bf27456b
DM
1992 };
1993
1994 # add default key first
971353e8 1995 &$cond_add_key($default_key, 1) if defined($default_key);
bf27456b 1996
d8c2b947
DM
1997 # add required keys first
1998 foreach my $key (sort keys %$data) {
1999 my $phash = $format->{$key};
2000 &$cond_add_key($key) if $phash && !$phash->{optional};
2001 }
2002
2003 # add the rest
bf27456b
DM
2004 foreach my $key (sort keys %$data) {
2005 &$cond_add_key($key);
2006 }
2007
2008 return $res;
2009}
2010
2011sub schema_get_type_text {
abc1afd8 2012 my ($phash, $style) = @_;
bf27456b 2013
32f8e0c7
DM
2014 my $type = $phash->{type} || 'string';
2015
bf27456b
DM
2016 if ($phash->{typetext}) {
2017 return $phash->{typetext};
2018 } elsif ($phash->{format_description}) {
2019 return "<$phash->{format_description}>";
2020 } elsif ($phash->{enum}) {
25d9bda9 2021 return "<" . join(' | ', sort @{$phash->{enum}}) . ">";
bf27456b
DM
2022 } elsif ($phash->{pattern}) {
2023 return $phash->{pattern};
32f8e0c7 2024 } elsif ($type eq 'integer' || $type eq 'number') {
05185ea2 2025 # NOTE: always access values as number (avoid converion to string)
bf27456b 2026 if (defined($phash->{minimum}) && defined($phash->{maximum})) {
25d9bda9 2027 return "<$type> (" . ($phash->{minimum} + 0) . " - " .
05185ea2 2028 ($phash->{maximum} + 0) . ")";
bf27456b 2029 } elsif (defined($phash->{minimum})) {
25d9bda9 2030 return "<$type> (" . ($phash->{minimum} + 0) . " - N)";
bf27456b 2031 } elsif (defined($phash->{maximum})) {
25d9bda9 2032 return "<$type> (-N - " . ($phash->{maximum} + 0) . ")";
bf27456b 2033 }
32f8e0c7 2034 } elsif ($type eq 'string') {
bf27456b
DM
2035 if (my $format = $phash->{format}) {
2036 $format = get_format($format) if ref($format) ne 'HASH';
2037 if (ref($format) eq 'HASH') {
abc1afd8
DM
2038 my $list_enums = 0;
2039 $list_enums = 1 if $style && $style eq 'config-sub';
2040 return generate_typetext($format, $list_enums);
bf27456b
DM
2041 }
2042 }
2043 }
2044
25d9bda9 2045 return "<$type>";
bf27456b
DM
2046}
2047
e143e9d8 20481;