+sub parse_size {
+ my ($value) = @_;
+
+ return undef if $value !~ m/^(\d+(\.\d+)?)([KMGT])?$/;
+ my ($size, $unit) = ($1, $3);
+ if ($unit) {
+ if ($unit eq 'K') {
+ $size = $size * 1024;
+ } elsif ($unit eq 'M') {
+ $size = $size * 1024 * 1024;
+ } elsif ($unit eq 'G') {
+ $size = $size * 1024 * 1024 * 1024;
+ } elsif ($unit eq 'T') {
+ $size = $size * 1024 * 1024 * 1024 * 1024;
+ }
+ }
+ return int($size);
+};
+
+sub format_size {
+ my ($size) = @_;
+
+ $size = int($size);
+
+ my $kb = int($size/1024);
+ return $size if $kb*1024 != $size;
+
+ my $mb = int($kb/1024);
+ return "${kb}K" if $mb*1024 != $kb;
+
+ my $gb = int($mb/1024);
+ return "${mb}M" if $gb*1024 != $mb;
+
+ my $tb = int($gb/1024);
+ return "${gb}G" if $tb*1024 != $gb;
+
+ return "${tb}T";
+};
+
+sub parse_property_string {
+ my ($format, $data, $path, $additional_properties) = @_;
+
+ # In property strings we default to not allowing additional properties
+ $additional_properties = 0 if !defined($additional_properties);
+
+ my $default_key;
+
+ my $res = {};
+ foreach my $part (split(/,/, $data)) {
+ next if $part =~ /^\s*$/;
+
+ if ($part =~ /^([^=]+)=(.+)$/) {
+ my ($k, $v) = ($1, $2);
+ die "duplicate key in comma-separated list property: $k\n" if defined($res->{$k});
+ my $schema = $format->{$k};
+ if (my $alias = $schema->{alias}) {
+ $k = $alias;
+ $schema = $format->{$k};
+ }
+ die "invalid key in comma-separated list property: $k\n" if !$schema;
+ if ($schema->{type} && $schema->{type} eq 'boolean') {
+ $v = 1 if $v =~ m/^(1|on|yes|true)$/i;
+ $v = 0 if $v =~ m/^(0|off|no|false)$/i;
+ }
+ $res->{$k} = $v;
+ } elsif ($part !~ /=/) {
+ die "duplicate key in comma-separated list property: $default_key\n" if $default_key;
+ foreach my $key (keys %$format) {
+ if ($format->{$key}->{default_key}) {
+ $default_key = $key;
+ if (!$res->{$default_key}) {
+ $res->{$default_key} = $part;
+ last;
+ }
+ die "duplicate key in comma-separated list property: $default_key\n";
+ }
+ }
+ die "value without key, but schema does not define a default key\n" if !$default_key;
+ } else {
+ die "missing key in comma-separated list property\n";
+ }
+ }
+
+ my $errors = {};
+ check_object($path, $format, $res, $additional_properties, $errors);
+ if (scalar(%$errors)) {
+ raise "format error\n", errors => $errors;
+ }
+
+ return $res;
+}
+
+sub print_property_string {
+ my ($data, $format, $skip, $path) = @_;
+
+ if (ref($format) ne 'HASH') {
+ my $schema = $format_list->{$format};
+ die "not a valid format: $format\n" if !$schema;
+ $format = $schema;
+ }
+
+ my $errors = {};
+ check_object($path, $format, $data, undef, $errors);
+ if (scalar(%$errors)) {
+ raise "format error", errors => $errors;
+ }
+
+ my $default_key;
+ my %skipped = map { $_ => 1 } @$skip;
+ my %allowed;
+ my %required; # this is a set, all present keys are required regardless of value
+ foreach my $key (keys %$format) {
+ $allowed{$key} = 1;
+ if (!$format->{$key}->{optional} && !$format->{$key}->{alias} && !$skipped{$key}) {
+ $required{$key} = 1;
+ }
+
+ # Skip default keys
+ if ($format->{$key}->{default_key}) {
+ if ($default_key) {
+ warn "multiple default keys in schema ($default_key, $key)\n";
+ } else {
+ $default_key = $key;
+ $skipped{$key} = 1;
+ }
+ }
+ }
+
+ my ($text, $comma);
+ if ($default_key) {
+ $text = "$data->{$default_key}";
+ $comma = ',';
+ } else {
+ $text = '';
+ $comma = '';
+ }
+
+ foreach my $key (sort keys %$data) {
+ delete $required{$key};
+ next if $skipped{$key};
+ die "invalid key: $key\n" if !$allowed{$key};
+
+ my $typeformat = $format->{$key}->{format};
+ my $value = $data->{$key};
+ next if !defined($value);
+ $text .= $comma;
+ $comma = ',';
+ if ($typeformat && $typeformat eq 'disk-size') {
+ $text .= "$key=" . format_size($value);
+ } else {
+ die "illegal value with commas for $key\n" if $value =~ /,/;
+ $text .= "$key=$value";
+ }
+ }
+
+ if (my $missing = join(',', keys %required)) {
+ die "missing properties: $missing\n";
+ }
+
+ return $text;
+}
+