summary refs log tree commit diff
path: root/src/nmcli/gen-metadata-nm-settings-nmcli.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nmcli/gen-metadata-nm-settings-nmcli.c')
-rw-r--r--src/nmcli/gen-metadata-nm-settings-nmcli.c520
1 files changed, 513 insertions, 7 deletions
diff --git a/src/nmcli/gen-metadata-nm-settings-nmcli.c b/src/nmcli/gen-metadata-nm-settings-nmcli.c
index 8e5831ac..babf1f06 100644
--- a/src/nmcli/gen-metadata-nm-settings-nmcli.c
+++ b/src/nmcli/gen-metadata-nm-settings-nmcli.c
@@ -3,9 +3,43 @@
 #include "libnm-client-aux-extern/nm-default-client.h"
 
 #include "libnmc-setting/nm-meta-setting-desc.h"
+#include "libnmc-setting/nm-meta-setting-base.h"
+#include "libnm-glib-aux/nm-enum-utils.h"
+#include "libnm-core-aux-intern/nm-common-macros.h"
+#include "nm-core-enum-types.h"
+#include <stdarg.h>
+#include <stdlib.h>
 
 #define INDENT 4
 
+#define prop_assert(cond, ...)        \
+    G_STMT_START                      \
+    if (!(cond))                      \
+        _prop_log(TRUE, __VA_ARGS__); \
+    G_STMT_END
+#define prop_abort(...) _prop_log(TRUE, __VA_ARGS__)
+#define prop_warn(...)  _prop_log(FALSE, __VA_ARGS__)
+
+static void G_GNUC_PRINTF(3, 4)
+    _prop_log(gboolean fatal, const NMMetaPropertyInfo *prop_info, const char *fmt, ...)
+{
+    va_list       vaargs;
+    gs_free char *msg      = NULL;
+    gs_free char *msg_full = NULL;
+
+    va_start(vaargs, fmt);
+    msg      = g_strdup_vprintf(fmt, vaargs);
+    msg_full = g_strdup_printf("gen-metadata-nm-settings-nmcli: %s.%s (type %d): %s\n",
+                               prop_info->setting_info->general->setting_name,
+                               prop_info->property_name,
+                               (int) prop_info->property_type->doc_format,
+                               msg);
+    if (fatal)
+        g_error("%s", msg_full);
+    else
+        g_warning("%s", msg_full);
+}
+
 static char *
 _xml_escape_attribute(const char *value)
 {
@@ -24,6 +58,448 @@ _indent_level(guint num_spaces)
     return &spaces[G_N_ELEMENTS(spaces) - num_spaces - 1];
 }
 
+static GType
+_property_get_gtype(const NMMetaPropertyInfo *prop_info)
+{
+    nm_auto_unref_gtypeclass NMSettingClass *setting_class =
+        g_type_class_ref(prop_info->setting_info->general->get_setting_gtype());
+    GParamSpec *param_spec =
+        g_object_class_find_property(G_OBJECT_CLASS(setting_class), prop_info->property_name);
+
+    if (param_spec)
+        return param_spec->value_type;
+    g_return_val_if_reached(G_TYPE_INVALID);
+}
+
+static char *
+get_enum_format(GType g_type)
+{
+    if (G_TYPE_IS_FLAGS(g_type))
+        return g_strdup_printf("flags (%s)", g_type_name(g_type));
+    else
+        return g_strdup_printf("choice (%s)", g_type_name(g_type));
+}
+
+static char *
+get_mac_format(const NMMetaPropertyInfo *prop_info)
+{
+    const NMMetaPropertyTypData *prop_typ_data = prop_info->property_typ_data;
+
+    if (prop_typ_data) {
+        switch (prop_typ_data->subtype.mac.mode) {
+        case NM_META_PROPERTY_TYPE_MAC_MODE_DEFAULT:
+        case NM_META_PROPERTY_TYPE_MAC_MODE_CLONED_ETHERNET:
+        case NM_META_PROPERTY_TYPE_MAC_MODE_CLONED_WIFI:
+            break;
+        case NM_META_PROPERTY_TYPE_MAC_MODE_INFINIBAND:
+            return g_strdup("Infiniband MAC address");
+        case NM_META_PROPERTY_TYPE_MAC_MODE_WPAN:
+            return g_strdup("WPAN MAC address");
+        default:
+            prop_abort(prop_info, "unknown mode (%d)", prop_typ_data->subtype.mac.mode);
+            break;
+        }
+    }
+
+    return g_strdup("MAC address");
+}
+
+static char *
+get_ethtool_format(const NMMetaPropertyInfo *prop_info)
+{
+    NMEthtoolID ethtool_id;
+
+    prop_assert(prop_info->property_typ_data, prop_info, "missing .property_typ_data");
+
+    ethtool_id = prop_info->property_typ_data->subtype.ethtool.ethtool_id;
+
+    switch (nm_ethtool_id_to_type(ethtool_id)) {
+    case NM_ETHTOOL_TYPE_CHANNELS:
+    case NM_ETHTOOL_TYPE_COALESCE:
+    case NM_ETHTOOL_TYPE_RING:
+        return g_strdup("integer");
+    case NM_ETHTOOL_TYPE_FEATURE:
+    case NM_ETHTOOL_TYPE_PAUSE:
+    case NM_ETHTOOL_TYPE_EEE:
+        return g_strdup("ternary");
+    case NM_ETHTOOL_TYPE_UNKNOWN:
+        nm_assert_not_reached();
+    };
+
+    return NULL;
+}
+
+static char *
+get_multilist_format(const NMMetaPropertyInfo *prop_info)
+{
+    NMMetaPropertyTypeFormat item_fmt;
+
+    if (prop_info->property_typ_data) {
+        item_fmt = prop_info->property_typ_data->list_items_doc_format;
+
+        switch (item_fmt) {
+        case NM_META_PROPERTY_TYPE_FORMAT_UNDEF:
+            prop_abort(prop_info, "undefined item format for multilist");
+            break;
+        case NM_META_PROPERTY_TYPE_FORMAT_INT:
+        case NM_META_PROPERTY_TYPE_FORMAT_MTU:
+            return g_strdup("list of integers");
+        case NM_META_PROPERTY_TYPE_FORMAT_STRING:
+            return g_strdup("list of strings");
+        case NM_META_PROPERTY_TYPE_FORMAT_BOOL:
+            return g_strdup("list of booleans");
+        case NM_META_PROPERTY_TYPE_FORMAT_TERNARY:
+            return g_strdup("list of ternaries");
+        case NM_META_PROPERTY_TYPE_FORMAT_MAC:
+            return g_strdup("list of MAC addresses");
+        case NM_META_PROPERTY_TYPE_FORMAT_IPV4:
+            return g_strdup("list of IPv4 addresses");
+        case NM_META_PROPERTY_TYPE_FORMAT_IPV6:
+            return g_strdup("list of IPv6 addresses");
+        default:
+            prop_abort(prop_info, "unsupported item format (%d)", item_fmt);
+            break;
+        }
+    }
+
+    return NULL;
+}
+
+static char *
+get_property_format(const NMMetaPropertyInfo *prop_info)
+{
+    const NMMetaPropertyType *prop_type = prop_info->property_type;
+    NMMetaPropertyTypeFormat  fmt       = prop_type->doc_format;
+
+    if (!prop_type->set_fcn)
+        return g_strdup("read only");
+
+    switch (fmt) {
+    case NM_META_PROPERTY_TYPE_FORMAT_INT:
+    case NM_META_PROPERTY_TYPE_FORMAT_MTU:
+        return g_strdup("integer");
+    case NM_META_PROPERTY_TYPE_FORMAT_STRING:
+        return g_strdup("string");
+    case NM_META_PROPERTY_TYPE_FORMAT_ENUM:
+        return get_enum_format(nm_meta_property_enum_get_type(prop_info));
+    case NM_META_PROPERTY_TYPE_FORMAT_SECRET_FLAGS:
+        return get_enum_format(NM_TYPE_SETTING_SECRET_FLAGS);
+    case NM_META_PROPERTY_TYPE_FORMAT_DCB_FLAGS:
+        return get_enum_format(NM_TYPE_SETTING_DCB_FLAGS);
+    case NM_META_PROPERTY_TYPE_FORMAT_BOOL:
+        return g_strdup("boolean");
+    case NM_META_PROPERTY_TYPE_FORMAT_TERNARY:
+        return g_strdup("ternary");
+    case NM_META_PROPERTY_TYPE_FORMAT_MAC:
+        return get_mac_format(prop_info);
+    case NM_META_PROPERTY_TYPE_FORMAT_IPV4:
+        return g_strdup("IPv4 address");
+    case NM_META_PROPERTY_TYPE_FORMAT_IPV6:
+        return g_strdup("IPv6 address");
+    case NM_META_PROPERTY_TYPE_FORMAT_BYTES:
+        return g_strdup("bytes");
+    case NM_META_PROPERTY_TYPE_FORMAT_PATH:
+        return g_strdup("filesystem path");
+    case NM_META_PROPERTY_TYPE_FORMAT_ETHTOOL:
+        return get_ethtool_format(prop_info);
+    case NM_META_PROPERTY_TYPE_FORMAT_MULTILIST:
+        return get_multilist_format(prop_info);
+    case NM_META_PROPERTY_TYPE_FORMAT_OBJLIST:
+        return g_strdup_printf("list of %s.%s objects",
+                               prop_info->setting_info->general->setting_name,
+                               prop_info->property_name);
+    case NM_META_PROPERTY_TYPE_FORMAT_OPTIONLIST:
+        return g_strdup("list of key/value options");
+    case NM_META_PROPERTY_TYPE_FORMAT_DCB:
+        return g_strdup("list of integers");
+    case NM_META_PROPERTY_TYPE_FORMAT_DCB_BOOL:
+        return g_strdup("list of booleans");
+    default:
+        prop_abort(prop_info, "missing .format");
+        return NULL;
+    }
+}
+
+#define append_vals(to, ...)                          \
+    G_STMT_START                                      \
+    {                                                 \
+        size_t      i;                                \
+        const char *from[] = {__VA_ARGS__};           \
+        for (i = 0; i < NM_N_ELEMENTS(from); i++)     \
+            g_ptr_array_add((to), g_strdup(from[i])); \
+    }                                                 \
+    G_STMT_END
+
+static void
+append_connection_types(GPtrArray *valid_values)
+{
+    size_t i;
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+        /* If the setting has a priority of a base-type, it's a valid value for connection.type */
+        NMSettingPriority pri =
+            _nm_setting_type_get_base_type_priority(nm_meta_setting_infos[i].get_setting_gtype());
+        if (pri != NM_SETTING_PRIORITY_INVALID)
+            g_ptr_array_add(valid_values, g_strdup(nm_meta_setting_infos[i].setting_name));
+    }
+}
+
+static void
+append_int_valid_range(const NMMetaPropertyInfo *prop_info, GPtrArray *valid_values)
+{
+    NMMetaSignUnsignInt64 min;
+    NMMetaSignUnsignInt64 max;
+
+    nm_meta_property_int_get_range(prop_info, &min, &max);
+    if (NM_IN_SET(_property_get_gtype(prop_info), G_TYPE_UINT, G_TYPE_UINT64)) {
+        g_ptr_array_add(
+            valid_values,
+            g_strdup_printf("%" G_GUINT64_FORMAT " - %" G_GUINT64_FORMAT, min.u64, max.u64));
+    } else {
+        g_ptr_array_add(
+            valid_values,
+            g_strdup_printf("%" G_GINT64_FORMAT " - %" G_GINT64_FORMAT, min.i64, max.i64));
+    }
+}
+
+static void
+_append_enum_valid_values(GType                       g_type,
+                          int                         min,
+                          int                         max,
+                          const NMUtilsEnumValueInfo *value_infos,
+                          GPtrArray                  *valid_values)
+{
+    size_t       i;
+    const char **alias;
+    GString     *names  = g_string_sized_new(64);
+    GArray      *values = _nm_utils_enum_get_values_full(g_type, min, max, value_infos);
+
+    for (i = 0; i < values->len; i++) {
+        NMUtilsEnumValueInfoFull *val = &nm_g_array_index(values, NMUtilsEnumValueInfoFull, i);
+
+        g_string_assign(names, val->nick);
+
+        if (val->aliases) {
+            for (alias = val->aliases; *alias; alias++) {
+                g_string_append_c(names, '/');
+                g_string_append(names, *alias);
+            }
+        }
+
+        g_ptr_array_add(valid_values, g_strdup_printf("%s (%s)", names->str, val->value_str));
+    }
+
+    g_string_free(names, TRUE);
+    g_array_unref(values);
+}
+
+static void
+append_enum_valid_values(const NMMetaPropertyInfo *prop_info, GPtrArray *valid_values)
+{
+    const NMMetaPropertyTypData *prop_typ_data = prop_info->property_typ_data;
+    const NMUtilsEnumValueInfo  *value_infos;
+    GType                        gtype;
+    int                          min;
+    int                          max;
+
+    gtype = nm_meta_property_enum_get_type(prop_info);
+    nm_meta_property_enum_get_range(prop_info, &min, &max);
+    value_infos = prop_typ_data ? prop_typ_data->subtype.gobject_enum.value_infos : NULL;
+
+    _append_enum_valid_values(gtype, min, max, value_infos, valid_values);
+}
+
+static void
+append_ethtool_valid_values(const NMMetaPropertyInfo *prop_info, GPtrArray *valid_values)
+{
+    NMEthtoolID ethtool_id;
+
+    prop_assert(prop_info->property_typ_data, prop_info, "missing .property_typ_data");
+
+    ethtool_id = prop_info->property_typ_data->subtype.ethtool.ethtool_id;
+
+    switch (nm_ethtool_id_to_type(ethtool_id)) {
+    case NM_ETHTOOL_TYPE_CHANNELS:
+    case NM_ETHTOOL_TYPE_COALESCE:
+    case NM_ETHTOOL_TYPE_RING:
+        g_ptr_array_add(valid_values, g_strdup_printf("0 - %u", G_MAXUINT32));
+        break;
+    case NM_ETHTOOL_TYPE_FEATURE:
+    case NM_ETHTOOL_TYPE_PAUSE:
+    case NM_ETHTOOL_TYPE_EEE:
+        append_vals(valid_values, "on", "off", "ignore");
+        break;
+    case NM_ETHTOOL_TYPE_UNKNOWN:
+        nm_assert_not_reached();
+    }
+}
+
+static void
+append_dcb_valid_values(const NMMetaPropertyInfo *prop_info, GPtrArray *valid_values)
+{
+    guint max;
+    guint other;
+
+    prop_assert(prop_info->property_typ_data, prop_info, "missing .property_typ_data");
+
+    max   = prop_info->property_typ_data->subtype.dcb.max;
+    other = prop_info->property_typ_data->subtype.dcb.other;
+
+    if (max != 0)
+        g_ptr_array_add(valid_values, g_strdup_printf("0 - %u", max));
+    if (other != 0 && (!max || other > max))
+        g_ptr_array_add(valid_values, g_strdup_printf("%u", other));
+}
+
+static GPtrArray *
+get_property_valid_values(const NMMetaPropertyInfo *prop_info)
+{
+    const NMMetaPropertyType    *prop_type     = prop_info->property_type;
+    const NMMetaPropertyTypData *prop_typ_data = prop_info->property_typ_data;
+    NMMetaPropertyTypeFormat     fmt           = prop_type->doc_format;
+    GPtrArray                   *valid_values  = g_ptr_array_new_full(16, g_free);
+
+    /* connection.type is generated differently */
+    if (prop_info->setting_info->general->meta_type == NM_META_SETTING_TYPE_CONNECTION
+        && !nm_strcmp0(prop_info->property_name, "type")) {
+        append_connection_types(valid_values);
+        return valid_values;
+    }
+
+    /* If it's a list, get the format of the items, but only  if we can process it
+     * (prop_typ_data->subtype is of type multilist, so we can't process ints or enums) */
+    if (fmt == NM_META_PROPERTY_TYPE_FORMAT_MULTILIST && prop_typ_data) {
+        switch (prop_typ_data->list_items_doc_format) {
+        case NM_META_PROPERTY_TYPE_FORMAT_SECRET_FLAGS:
+        case NM_META_PROPERTY_TYPE_FORMAT_DCB_FLAGS:
+        case NM_META_PROPERTY_TYPE_FORMAT_BOOL:
+        case NM_META_PROPERTY_TYPE_FORMAT_DCB_BOOL:
+        case NM_META_PROPERTY_TYPE_FORMAT_TERNARY:
+        case NM_META_PROPERTY_TYPE_FORMAT_ETHTOOL:
+            fmt = prop_typ_data->list_items_doc_format;
+            break;
+        case NM_META_PROPERTY_TYPE_FORMAT_ENUM:
+            prop_warn(prop_info, "unknown enum type, can't get valid values");
+            break;
+        case NM_META_PROPERTY_TYPE_FORMAT_INT:
+            prop_warn(prop_info, "can't check for valid range in multilists");
+            break;
+        default:
+            /* Other types probably don't need to show "Valid values" */
+            break;
+        }
+    }
+
+    switch (fmt) {
+    case NM_META_PROPERTY_TYPE_FORMAT_INT:
+        append_int_valid_range(prop_info, valid_values);
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_ENUM:
+        append_enum_valid_values(prop_info, valid_values);
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_SECRET_FLAGS:
+        _append_enum_valid_values(NM_TYPE_SETTING_SECRET_FLAGS, 0, G_MAXUINT, NULL, valid_values);
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_DCB_FLAGS:
+        _append_enum_valid_values(NM_TYPE_SETTING_DCB_FLAGS, 0, G_MAXUINT, NULL, valid_values);
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_BOOL:
+    case NM_META_PROPERTY_TYPE_FORMAT_DCB_BOOL:
+        append_vals(valid_values, "true/yes/on", "false/no/off");
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_TERNARY:
+        append_vals(valid_values, "true/yes/on", "false/no/off", "default/unknown");
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_ETHTOOL:
+        append_ethtool_valid_values(prop_info, valid_values);
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_DCB:
+        append_dcb_valid_values(prop_info, valid_values);
+        break;
+    default:
+        break;
+    }
+
+    if (prop_typ_data && prop_typ_data->values_static) {
+        const char *const *v;
+        for (v = prop_typ_data->values_static; *v; v++)
+            g_ptr_array_add(valid_values, g_strdup(*v));
+    }
+
+    return valid_values;
+}
+
+static void
+append_int_special_values(const NMMetaPropertyInfo *prop_info, GPtrArray *special_values)
+{
+    const NMMetaPropertyTypData *prop_typ_data = prop_info->property_typ_data;
+
+    if (prop_typ_data && prop_typ_data->subtype.gobject_int.value_infos) {
+        GType                          gtype   = _property_get_gtype(prop_info);
+        bool                           is_uint = NM_IN_SET(gtype, G_TYPE_UINT, G_TYPE_UINT64);
+        guint                          base    = prop_typ_data->subtype.gobject_int.base;
+        const NMMetaUtilsIntValueInfo *v       = prop_typ_data->subtype.gobject_int.value_infos;
+
+        if (!(base == 0 || base == 10 || (is_uint && base == 16))) {
+            if (is_uint)
+                prop_abort(prop_info, "only base 10 supported for signed int");
+            else
+                prop_abort(prop_info, "only base 10 or 16 supported for uint");
+        }
+
+        for (; v->nick != NULL; v++) {
+            char *v_str;
+            if (base == 16)
+                v_str = g_strdup_printf("%s (0x%" G_GINT64_MODIFIER "x)", v->nick, v->value.u64);
+            else if (is_uint)
+                v_str = g_strdup_printf("%s (%" G_GUINT64_FORMAT ")", v->nick, v->value.u64);
+            else
+                v_str = g_strdup_printf("%s (%" G_GINT64_FORMAT ")", v->nick, v->value.i64);
+
+            g_ptr_array_add(special_values, v_str);
+        }
+    }
+}
+
+static GPtrArray *
+get_property_special_values(const NMMetaPropertyInfo *prop_info)
+{
+    const NMMetaPropertyType    *prop_type      = prop_info->property_type;
+    const NMMetaPropertyTypData *prop_typ_data  = prop_info->property_typ_data;
+    NMMetaPropertyTypeFormat     fmt            = prop_type->doc_format;
+    GPtrArray                   *special_values = g_ptr_array_new_full(16, g_free);
+
+    switch (fmt) {
+    case NM_META_PROPERTY_TYPE_FORMAT_INT:
+        append_int_special_values(prop_info, special_values);
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_MAC:
+        if (prop_typ_data) {
+            switch (prop_typ_data->subtype.mac.mode) {
+            case NM_META_PROPERTY_TYPE_MAC_MODE_CLONED_ETHERNET:
+            case NM_META_PROPERTY_TYPE_MAC_MODE_CLONED_WIFI:
+                append_vals(special_values,
+                            NM_CLONED_MAC_PRESERVE,
+                            NM_CLONED_MAC_PERMANENT,
+                            NM_CLONED_MAC_RANDOM,
+                            NM_CLONED_MAC_STABLE);
+                if (prop_typ_data->subtype.mac.mode == NM_META_PROPERTY_TYPE_MAC_MODE_CLONED_WIFI)
+                    append_vals(special_values, NM_CLONED_MAC_STABLE_SSID);
+                break;
+            default:
+                break;
+            }
+        }
+        break;
+    case NM_META_PROPERTY_TYPE_FORMAT_MTU:
+        g_ptr_array_add(special_values, g_strdup("auto"));
+        break;
+    default:
+        break;
+    }
+
+    return special_values;
+}
+
 int
 main(int argc, char *argv[])
 {
@@ -46,17 +522,22 @@ main(int argc, char *argv[])
         g_print(" >\n");
 
         for (i_property = 0; i_property < sett_info->properties_num; i_property++) {
-            const NMMetaPropertyInfo *prop_info = sett_info->properties[i_property];
-            gs_free char             *tmp2      = NULL;
-            gs_free char             *tmp3      = NULL;
-            gs_free char             *tmp4      = NULL;
+            const NMMetaPropertyInfo    *prop_info    = sett_info->properties[i_property];
+            gs_free char                *name         = NULL;
+            gs_free char                *alias        = NULL;
+            gs_free char                *descr        = NULL;
+            gs_free char                *fmt          = NULL;
+            gs_unref_ptrarray GPtrArray *vals_arr     = NULL;
+            gs_free char                *vals_str     = NULL;
+            gs_unref_ptrarray GPtrArray *specials_arr = NULL;
+            gs_free char                *specials_str = NULL;
 
             g_print("%s<property", _indent_level(2 * INDENT));
-            g_print(" name=%s", tmp2 = _xml_escape_attribute(prop_info->property_name));
+            g_print(" name=%s", name = _xml_escape_attribute(prop_info->property_name));
             if (prop_info->property_alias)
                 g_print("\n%salias=%s",
                         _indent_level(2 * INDENT + 10),
-                        tmp3 = _xml_escape_attribute(prop_info->property_alias));
+                        alias = _xml_escape_attribute(prop_info->property_alias));
             if (prop_info->describe_doc) {
                 /* These descriptions are used by interactive nmcli modes. For the most part,
                  * they are themselves generated (see "settings-docs.h"). Some of them are instead
@@ -71,8 +552,33 @@ main(int argc, char *argv[])
                  * Name them something else ("<nmcli-description>") which isn't actually used. */
                 g_print("\n%snmcli-description=%s",
                         _indent_level(2 * INDENT + 10),
-                        tmp4 = _xml_escape_attribute(prop_info->describe_doc));
+                        descr = _xml_escape_attribute(prop_info->describe_doc));
             }
+
+            fmt = get_property_format(prop_info);
+            if (fmt)
+                g_print("\n%sformat=%s",
+                        _indent_level(2 * INDENT + 10),
+                        _xml_escape_attribute(fmt));
+
+            vals_arr = get_property_valid_values(prop_info);
+            if (vals_arr->len) {
+                g_ptr_array_add(vals_arr, NULL);
+                vals_str = g_strjoinv(", ", (char **) vals_arr->pdata);
+                g_print("\n%svalues=%s",
+                        _indent_level(2 * INDENT + 10),
+                        _xml_escape_attribute(vals_str));
+            }
+
+            specials_arr = get_property_special_values(prop_info);
+            if (specials_arr->len) {
+                g_ptr_array_add(specials_arr, NULL);
+                specials_str = g_strjoinv(", ", (char **) specials_arr->pdata);
+                g_print("\n%sspecial-values=%s",
+                        _indent_level(2 * INDENT + 10),
+                        _xml_escape_attribute(specials_str));
+            }
+
             g_print(" />\n");
         }