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