/* * Copyright (C) 2012 Intel Corporation * * Authors: Krzesimir Nowak * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include "fragment-util.h" #include "xml-util.h" typedef struct { gchar *node_name; gchar *attribute_name; } NodeDiff; static NodeDiff * node_diff_new (const xmlChar *node_name, const xmlChar *attribute_name) { NodeDiff *diff = g_slice_new (NodeDiff); diff->node_name = g_strdup ((gchar *) node_name); diff->attribute_name = g_strdup ((gchar *) attribute_name); return diff; } static void node_diff_free (NodeDiff *diff) { if (diff != NULL) { g_free (diff->node_name); g_free (diff->attribute_name); g_slice_free (NodeDiff, diff); } } static GList * get_toplevel_changes (xmlNodePtr current_node, xmlNodePtr new_node) { xmlAttrPtr attribute; GHashTable *current_attributes = xml_util_get_attributes_map (current_node); GList *changes = NULL; const xmlChar *name = new_node->name; /* compare attributes */ for (attribute = new_node->properties; attribute != NULL; attribute = attribute->next) { const xmlChar *value = NULL; const xmlChar *key = attribute->name; gboolean differs = FALSE; if (g_hash_table_lookup_extended (current_attributes, key, NULL, (gpointer *) &value)) { if (xmlStrcmp (value, attribute->children->content)) differs = TRUE; g_hash_table_remove (current_attributes, key); } else differs = TRUE; if (differs) changes = g_list_prepend (changes, node_diff_new (name, key)); } if (g_hash_table_size (current_attributes) > 0) { GHashTableIter iter; xmlChar *key = NULL; g_hash_table_iter_init (&iter, current_attributes); while (g_hash_table_iter_next (&iter, (gpointer *) &key, NULL)) changes = g_list_prepend (changes, node_diff_new (name, key)); } g_hash_table_unref (current_attributes); return changes; } #if GLIB_CHECK_VERSION (2, 32, 0) #define hash_table_contains g_hash_table_contains #define hash_table_add g_hash_table_add #else static gboolean hash_table_contains (GHashTable *table, gpointer key) { return g_hash_table_lookup_extended (table, key, NULL, NULL); } static void hash_table_add (GHashTable *table, gpointer key) { g_hash_table_replace (table, key, key); } #endif static gboolean is_read_only (const gchar *changed_element, const gchar *changed_attribute) { static GHashTable *readonly_props = NULL; static gsize readonly_props_loaded = 0; if (g_once_init_enter (&readonly_props_loaded)) { readonly_props = g_hash_table_new (g_str_hash, g_str_equal); hash_table_add (readonly_props, (gpointer) "@id"); hash_table_add (readonly_props, (gpointer) "@parentID"); hash_table_add (readonly_props, (gpointer) "@refID"); hash_table_add (readonly_props, (gpointer) "@restricted"); hash_table_add (readonly_props, (gpointer) "@searchable"); hash_table_add (readonly_props, (gpointer) "@childCount"); hash_table_add (readonly_props, (gpointer) "searchClass"); hash_table_add (readonly_props, (gpointer) "searchClass@name"); hash_table_add (readonly_props, (gpointer) "searchClass@includeDerived"); hash_table_add (readonly_props, (gpointer) "createClass"); hash_table_add (readonly_props, (gpointer) "createClass@name"); hash_table_add (readonly_props, (gpointer) "createClass@includeDerived"); hash_table_add (readonly_props, (gpointer) "writeStatus"); hash_table_add (readonly_props, (gpointer) "res@importUri"); hash_table_add (readonly_props, (gpointer) "storageTotal"); hash_table_add (readonly_props, (gpointer) "storageUsed"); hash_table_add (readonly_props, (gpointer) "storageFree"); hash_table_add (readonly_props, (gpointer) "storageMaxPartition"); hash_table_add (readonly_props, (gpointer) "storageMedium"); hash_table_add (readonly_props, (gpointer) "playbackCount"); hash_table_add (readonly_props, (gpointer) "srsRecordScheduleID"); hash_table_add (readonly_props, (gpointer) "srsRecordTaskID"); hash_table_add (readonly_props, (gpointer) "price"); hash_table_add (readonly_props, (gpointer) "price@currency"); hash_table_add (readonly_props, (gpointer) "payPerView"); hash_table_add (readonly_props, (gpointer) "dateTimeRange"); hash_table_add (readonly_props, (gpointer) "dateTimeRange@daylightSaving"); hash_table_add (readonly_props, (gpointer) "signalStrength"); hash_table_add (readonly_props, (gpointer) "signalLocked"); hash_table_add (readonly_props, (gpointer) "tuned"); hash_table_add (readonly_props, (gpointer) "containerUpdateID"); hash_table_add (readonly_props, (gpointer) "objectUpdateID"); hash_table_add (readonly_props, (gpointer) "totalDeletedChildCount"); hash_table_add (readonly_props, (gpointer) "res@updateCount"); g_once_init_leave (&readonly_props_loaded, 1); } if (changed_element != NULL) { if (changed_attribute != NULL) { gchar *test_prop = g_strdup_printf ("%s@%s", changed_element, changed_attribute); gboolean result = hash_table_contains (readonly_props, test_prop); g_free (test_prop); if (result) return TRUE; test_prop = g_strdup_printf ("@%s", changed_attribute); result = hash_table_contains (readonly_props, test_prop); g_free (test_prop); if (result) return TRUE; } return hash_table_contains (readonly_props, changed_element); } return FALSE; } static gboolean is_any_change_read_only (xmlNodePtr current_node, xmlNodePtr new_node) { GList *changes = get_toplevel_changes (current_node, new_node); GList *iter; gboolean read_only = FALSE; for (iter = changes; iter != NULL; iter = iter->next) { NodeDiff *diff = (NodeDiff *) iter->data; if (is_read_only (diff->node_name, diff->attribute_name)) { read_only = TRUE; break; } } if (changes != NULL) g_list_free_full (changes, (GDestroyNotify) node_diff_free); return read_only; } static GUPnPDIDLLiteFragmentResult apply_temporary_modification (DocNode *modified, xmlNodePtr current_node, xmlNodePtr new_node, XSDData *xsd_data) { xmlNodePtr mod_cur_node = xml_util_find_node (modified->node, current_node); xmlNodePtr new_node_copy = xml_util_copy_node (new_node); if (mod_cur_node == NULL) { return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR; } xmlUnlinkNode (new_node_copy); mod_cur_node = xmlReplaceNode (mod_cur_node, new_node_copy); xmlUnlinkNode (mod_cur_node); xmlFreeNode (mod_cur_node); if (!xsd_data_validate_doc (xsd_data, modified->doc)) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_NEW_INVALID; return GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK; } static GUPnPDIDLLiteFragmentResult apply_temporary_addition (DocNode *modified, xmlNodePtr sibling, xmlNodePtr new_node, XSDData *xsd_data) { xmlNodePtr mod_sibling; xmlNodePtr new_node_copy = xml_util_copy_node (new_node); if (sibling->doc == modified->doc) mod_sibling = sibling; else mod_sibling = xml_util_find_node (modified->node, sibling); if (mod_sibling == NULL) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR; xmlUnlinkNode (new_node_copy); if (xmlAddNextSibling (mod_sibling, new_node_copy) == NULL) { xmlFreeNode (new_node_copy); return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR; } if (!xsd_data_validate_doc (xsd_data, modified->doc)) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_NEW_INVALID; return GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK; } static GUPnPDIDLLiteFragmentResult apply_temporary_removal (DocNode *modified, xmlNodePtr current_node, XSDData *xsd_data) { xmlNodePtr mod_cur_node = xml_util_find_node (modified->node, current_node); if (mod_cur_node == NULL) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR; xmlUnlinkNode (mod_cur_node); xmlFreeNode (mod_cur_node); if (!xsd_data_validate_doc (xsd_data, modified->doc)) /* not sure if this is correct */ return GUPNP_DIDL_LITE_FRAGMENT_RESULT_REQUIRED_TAG; return GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK; } typedef struct { gboolean required; GHashTable* required_dep_props; /* string set */ GHashTable* required_indep_props; /* string to indep prop */ } IndependentProperty; static void independent_property_free (IndependentProperty *indep) { if (indep != NULL) { g_hash_table_unref (indep->required_dep_props); g_hash_table_unref (indep->required_indep_props); g_slice_free (IndependentProperty, indep); } } static IndependentProperty * independent_property_new (gboolean required) { IndependentProperty *indep = g_slice_new (IndependentProperty); indep->required = required; indep->required_dep_props = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); indep->required_indep_props = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) independent_property_free); return indep; } static void insert_indep_prop (GHashTable *props, const gchar *name, IndependentProperty *prop) { g_hash_table_insert (props, g_strdup (name), prop); } static void insert_indep_prop_to_indep (IndependentProperty *prop, const gchar *name, IndependentProperty *req_prop) { insert_indep_prop (prop->required_indep_props, name, req_prop); } static void add_dep_prop (IndependentProperty *indep, const gchar *name) { hash_table_add (indep->required_dep_props, g_strdup (name)); } static IndependentProperty * create_prop_with_required_dep_props (gboolean required, const gchar *dep_prop, ...) { IndependentProperty *indep = independent_property_new (required); if (dep_prop != NULL) { va_list var_args; const gchar *name = dep_prop; va_start (var_args, dep_prop); do { add_dep_prop (indep, name); name = va_arg (var_args, gchar *); } while (name != NULL); va_end (var_args); } return indep; } static IndependentProperty * create_foreign_metadata_props (void) { IndependentProperty *fm = independent_property_new (FALSE); IndependentProperty *other; add_dep_prop (fm, "type"); other = independent_property_new (TRUE); insert_indep_prop_to_indep (fm, "fmId", other); other = independent_property_new (TRUE); insert_indep_prop_to_indep (fm, "fmClass", other); other = independent_property_new (TRUE); insert_indep_prop_to_indep (fm, "fmProvider", other); other = independent_property_new (TRUE); add_dep_prop (other, "xmlFlag"); insert_indep_prop_to_indep (fm, "fmBody", other); return fm; } static GHashTable * get_required_properties (void) { static GHashTable *required_props = NULL; static gsize required_props_loaded = 0; if (g_once_init_enter (&required_props_loaded)) { required_props = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) independent_property_free); insert_indep_prop (required_props, "", create_prop_with_required_dep_props (FALSE, "id", "parentID", "restricted", NULL)); insert_indep_prop (required_props, "title", independent_property_new (TRUE)); insert_indep_prop (required_props, "class", independent_property_new (TRUE)); insert_indep_prop (required_props, "res", create_prop_with_required_dep_props (FALSE, "protocolInfo", NULL)); insert_indep_prop (required_props, "programID", create_prop_with_required_dep_props (FALSE, "type", NULL)); insert_indep_prop (required_props, "seriesID", create_prop_with_required_dep_props (FALSE, "type", NULL)); insert_indep_prop (required_props, "channelID", create_prop_with_required_dep_props (FALSE, "type", NULL)); insert_indep_prop (required_props, "programCode", create_prop_with_required_dep_props (FALSE, "type", NULL)); insert_indep_prop (required_props, "channelGroupName", create_prop_with_required_dep_props (FALSE, "id", NULL)); insert_indep_prop (required_props, "price", create_prop_with_required_dep_props (FALSE, "currency", NULL)); insert_indep_prop (required_props, "desc", create_prop_with_required_dep_props (FALSE, "nameSpace", NULL)); insert_indep_prop (required_props, "deviceUDN", create_prop_with_required_dep_props (FALSE, "serviceType", "serviceId", NULL)); insert_indep_prop (required_props, "stateVariableCollection", create_prop_with_required_dep_props (FALSE, "serviceName", "rcsInstanceType", NULL)); insert_indep_prop (required_props, "foreignMetadata", create_foreign_metadata_props ()); g_once_init_leave (&required_props_loaded, 1); } return required_props; } static gboolean is_required (const xmlChar *changed_element, const xmlChar *changed_attribute) { GHashTable *required_props = get_required_properties (); if (changed_element != NULL) { IndependentProperty *toplevel_prop = g_hash_table_lookup (required_props, ""); IndependentProperty *this_prop = g_hash_table_lookup (required_props, (gpointer) changed_element); if (changed_attribute != NULL) { if (hash_table_contains (toplevel_prop->required_dep_props, changed_attribute)) return TRUE; if (hash_table_contains (this_prop->required_dep_props, changed_attribute)) return TRUE; } if (hash_table_contains (toplevel_prop->required_indep_props, changed_element)) return TRUE; /* TODO: check if changed element is not a required * property of its parent element. That needs some * additions in IndepependentProperty. */ } return FALSE; } static GUPnPDIDLLiteFragmentResult new_doc_is_valid_modification (DocNode *modified, xmlDocPtr current_doc, xmlDocPtr new_doc, XSDData *xsd_data) { xmlNodePtr current_node = current_doc->children->children; xmlNodePtr new_node = new_doc->children->children; xmlNodePtr last_sibling = NULL; while (current_node != NULL && new_node != NULL) { GUPnPDIDLLiteFragmentResult result; xmlNodePtr temp_current_node = current_node; xmlNodePtr temp_new_node = new_node; last_sibling = new_node; /* We can't put this line into for instruction, * because new_node could be unlinked from its * document and put into another one in * apply_temporary_modification. We have to get its * sibling before that could happen. */ new_node = new_node->next; current_node = current_node->next; if (xml_util_node_deep_equal (temp_current_node, temp_new_node)) /* This is just a context, skip the checks. */ continue; if (xmlStrcmp (temp_current_node->name, temp_new_node->name)) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_NEW_INVALID; if (is_any_change_read_only (temp_current_node, temp_new_node)) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_READONLY_TAG; result = apply_temporary_modification (modified, temp_current_node, temp_new_node, xsd_data); if (result != GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK) return result; } if (last_sibling == NULL) { if (modified->node->children != NULL) last_sibling = modified->node->last; else /* We expect that modified object has some * required tags like or * . */ return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR; } /* If there are some more nodes in current fragment then it * means they are going to be removed. Check against required * or read-only tag removal. */ while (current_node != NULL) { GUPnPDIDLLiteFragmentResult result; xmlNodePtr temp_node = current_node; current_node = current_node->next; /* TODO: should we check if there are some readonly * attributes when we remove whole element? */ if (is_read_only ((gchar *) temp_node->name, NULL)) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_READONLY_TAG; /* We don't check for required attributes or * subelements, because most of them are required only * when the element exists. And we are removing this * one. */ if (is_required (temp_node->name, NULL)) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_REQUIRED_TAG; result = apply_temporary_removal (modified, temp_node, xsd_data); if (result != GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK) return result; } /* If there are some more nodes in new fragment then it means * they are going to be added. Check against read-only tags * addition and general sanity check. */ while (new_node != NULL) { GUPnPDIDLLiteFragmentResult result; xmlNodePtr temp_node; if (is_read_only ((gchar *) new_node->name, NULL)) return GUPNP_DIDL_LITE_FRAGMENT_RESULT_READONLY_TAG; /* TODO: We probably should check if newly added node * has all required properties. Maybe XSD check could * do that for us. */ temp_node = new_node; new_node = new_node->next; result = apply_temporary_addition (modified, last_sibling, temp_node, xsd_data); if (result != GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK) return result; } return GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK; } static gchar * fix_fragment (const gchar *fragment) { return g_strdup_printf ("\n" "%s\n", fragment); } static gboolean is_current_doc_part_of_original_doc (DocNode *original, xmlDocPtr current_doc) { xmlNodePtr current_node = current_doc->children->children; xmlNodePtr this_node; /* No current node means that we want to add new elements to the document. */ if (current_node == NULL) return TRUE; this_node = xml_util_find_node (original->node, current_node); if (this_node == NULL) return FALSE; for (current_node = current_node->next, this_node = this_node->next; current_node != NULL && this_node != NULL; current_node = current_node->next, this_node = this_node->next) if (!xml_util_node_deep_equal (current_node, this_node)) return FALSE; return TRUE; } GUPnPDIDLLiteFragmentResult fragment_util_check_fragments (DocNode *original, DocNode *modified, const gchar *current_fragment, const gchar *new_fragment, XSDData *xsd_data) { gchar *fixed_current_fragment = fix_fragment (current_fragment); gchar *fixed_new_fragment = fix_fragment (new_fragment); xmlDocPtr current_doc = xmlReadDoc (BAD_CAST (fixed_current_fragment), NULL, NULL, XML_PARSE_NONET); xmlDocPtr new_doc = xmlReadDoc (BAD_CAST (fixed_new_fragment), NULL, NULL, XML_PARSE_NONET); GUPnPDIDLLiteFragmentResult result; if (current_doc == NULL || current_doc->children == NULL) { result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_CURRENT_BAD_XML; goto out; } if (new_doc == NULL || new_doc->children == NULL) { result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_NEW_BAD_XML; goto out; } /* Assuming current_doc->children is non-NULL. */ if (current_doc->children->children != NULL) { /* If the child element is title or class, * it must not be set to empty or removed. */ if (g_strrstr ((char *) current_doc->children->children->name, "title") != NULL || g_strrstr ((char *) current_doc->children->children->name, "class") != NULL) { /* If the new tag has no corresponding title or class element */ if (new_doc->children->children == NULL) { result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_REQUIRED_TAG; goto out; } /* If the new tag has an empty value for title or class */ if (new_doc->children->children->children == NULL) { result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_REQUIRED_TAG; goto out; } } } if (!is_current_doc_part_of_original_doc (original, current_doc)) { result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_CURRENT_INVALID; goto out; } result = new_doc_is_valid_modification (modified, current_doc, new_doc, xsd_data); out: if (new_doc != NULL) xmlFreeDoc (new_doc); if (current_doc != NULL) xmlFreeDoc (current_doc); g_free (fixed_new_fragment); g_free (fixed_current_fragment); return result; } static const gchar * get_data_dir (void) { const gchar *datadir = g_getenv ("GUPNP_AV_DATADIR"); if (datadir == NULL) /* that's a macro defined by -DDATADIR=foo */ datadir = DATADIR; return datadir; } static gchar * get_xsd_path (const gchar *path) { return g_strdup_printf ("%s" G_DIR_SEPARATOR_S "%s", get_data_dir (), path); } static xmlParserInputPtr our_own_loader (const char *url, const char *id, xmlParserCtxtPtr context) { gchar *basename; gchar *path; xmlParserInputPtr input; g_debug ("URL: %s, ID: %s.", url, id); basename = g_path_get_basename (url); path = get_xsd_path (basename); g_debug ("BASENAME: %s, PATH: %s", basename, path); input = xmlNewInputFromFile (context, path); g_free (basename); g_free (path); return input; } XSDData * fragment_util_get_didl_lite_xsd_data (void) { gchar *path = get_xsd_path ("didl-lite-v2.xsd"); xmlExternalEntityLoader original_loader = xmlGetExternalEntityLoader (); XSDData *xsd_data; xmlSetExternalEntityLoader (our_own_loader); xsd_data = xsd_data_new (path); xmlSetExternalEntityLoader (original_loader); g_free (path); return xsd_data; } gboolean fragment_util_apply_modification (xmlNodePtr *node_ptr, DocNode *modified) { xmlNodePtr node_copy; xmlNodePtr old; if (node_ptr == NULL || *node_ptr == NULL) return FALSE; node_copy = xml_util_copy_node (modified->node); if (node_copy == NULL) return FALSE; old = xmlReplaceNode (*node_ptr, node_copy); if (old == NULL) return FALSE; *node_ptr = node_copy; xmlFreeNode (old); return TRUE; }