#include #include #include "../gskerror.h" #include "gskxmlvaluereader.h" /* * XXX: GMarkup assumes UTF-8, so this isn't 8-bit clean (not to mention * string-escaping issues in GskXmlValueWriter). */ /* * * Helpers. * */ static __inline__ void g_value_set_string_len (GValue *value, const char *str, guint len) { char *tmp; tmp = g_alloca (len + 1); memcpy (tmp, str, len); tmp[len] = 0; g_value_set_string (value, tmp); } static __inline__ void g_value_move (GValue *dst, GValue *src) { memcpy (dst, src, sizeof (*dst)); memset (src, 0, sizeof (*src)); } /* Compare canonical versions of property names. */ static gboolean property_names_equal (const char *pa, const char *pb) { g_return_val_if_fail (pa && pb, FALSE); for ( ; ; ++pa, ++pb) { char a = *pa; char b = *pb; if (a == '\0') return b ? FALSE : TRUE; else if (b == '\0') return FALSE; else if (a != b) { /* Only if both a and b are equivalent to '-', are they still * equal, so if either one is in the non-equivalent class, * they are not equal. */ if (g_ascii_isalnum (a) || g_ascii_isalnum (b)) return FALSE; } } } static gboolean parse_text_value (GValue *value, const char *str, int len, GError **error) { GType type = G_VALUE_TYPE (value); switch (type) { case G_TYPE_CHAR: if (len == 0) return FALSE; g_value_set_char (value, *str); return TRUE; case G_TYPE_UCHAR: if (len == 0) return FALSE; g_value_set_uchar (value, *str); return TRUE; case G_TYPE_BOOLEAN: { if (len == 0) return FALSE; switch (*str) { case 'y': case 'Y': case 't': case 'T': case '1': g_value_set_boolean (value, TRUE); return TRUE; case 'n': case 'N': case 'f': case 'F': case '0': g_value_set_boolean (value, FALSE); return TRUE; } return FALSE; } #define STRTOX_TYPE_CASE(T,t,fct) \ case G_TYPE_##T: \ { \ char *tmp, *end; \ int base; \ g##t parsed_val; \ if (len > 1023) \ len = 1023; \ tmp = g_alloca (len + 1); \ memcpy (tmp, str, len); \ tmp[len] = 0; \ base = (tmp[0] == '0' && tmp[1] == 'x') ? 16 : 10; \ parsed_val = (g##t) fct (tmp, &end, base); \ if (tmp == end) \ return FALSE; \ g_value_set_##t (value, parsed_val); \ } \ break; STRTOX_TYPE_CASE (INT, int, strtol) STRTOX_TYPE_CASE (UINT, uint, strtoul) STRTOX_TYPE_CASE (LONG, long, strtol) STRTOX_TYPE_CASE (ULONG, ulong, strtoul) STRTOX_TYPE_CASE (INT64, int64, strtoll) STRTOX_TYPE_CASE (UINT64, uint64, strtoull) #undef STRTOX_TYPE_CASE case G_TYPE_FLOAT: case G_TYPE_DOUBLE: { char *tmp; char *end; if (len > 1023) len = 1023; tmp = g_alloca (len + 1); memcpy (tmp, str, len); tmp[len] = 0; switch (type) { case G_TYPE_FLOAT: g_value_set_float (value, (gfloat) strtod (tmp, &end)); break; case G_TYPE_DOUBLE: g_value_set_double (value, (gdouble) strtod (tmp, &end)); break; } if (tmp == end) return FALSE; } break; case G_TYPE_STRING: g_value_set_string_len (value, str, len); break; /* TODO: FLAGS, ENUMS */ default: { /* See if we can use g_value_transform to parse type * from a string. */ if (g_value_type_transformable (G_TYPE_STRING, type)) { GValue tmp_value = { 0, { { 0 }, { 0 } } }; gboolean result; g_value_init (&tmp_value, G_TYPE_STRING); g_value_set_string_len (&tmp_value, str, len); result = g_value_transform (&tmp_value, value); if (!result) { g_set_error (error, GSK_G_ERROR_DOMAIN, 0, /* TODO: error code */ "error transforming string '%s' to a %s", g_value_get_string (&tmp_value), g_type_name (type)); } g_value_unset (&tmp_value); return result; } else { g_set_error (error, GSK_G_ERROR_DOMAIN, 0, /* TODO: error code */ "cannot parse value of type %s", g_type_name (type)); return FALSE; } } } return TRUE; } /* * * XmlStackFrame * * */ /* Our little grammar/state diagram: * * VALUE: OBJECT-BODY * | VALUE-TEXT * | TEXT * * OBJECT-BODY: [push] VALUE [pop] OBJECT-BODY * | empty * * VALUE-TEXT: TEXT */ typedef enum _XmlState { STATE_VALUE, /* expecting VALUE */ STATE_OBJECT_BODY, /* expecting OBJECT-BODY */ STATE_PROPERTY_CLOSE, /* expecting */ STATE_VALUE_TEXT, /* expecting TEXT */ STATE_VALUE_CLOSE /* expecting */ } XmlState; typedef struct _XmlStackFrame XmlStackFrame; struct _XmlStackFrame { /* Current scanning state. */ XmlState state; /* What type we're supposed to instantiate in this frame * (or G_TYPE_INVALID for a top-level value of unspecified type). */ GType type; /* The value being constructed. */ GValue value; /* If value is an object, properties to be set at construction: */ GArray *properties; /* of GParameter */ /* If value is an object and we've pushed a new stack frame for * one of its properties, this is the corresponding GParamSpec. * (We assume this cannot go away, so we don't reference it.) */ GParamSpec *param_spec; /* Parent stack frame. */ XmlStackFrame *parent; }; static GMemChunk *xml_stack_frame_chunk = NULL; G_LOCK_DEFINE_STATIC (xml_stack_frame_chunk); static __inline__ XmlStackFrame * xml_stack_frame_alloc0 (void) { XmlStackFrame *frame; G_LOCK (xml_stack_frame_chunk); if (xml_stack_frame_chunk == NULL) { xml_stack_frame_chunk = g_mem_chunk_create (XmlStackFrame, 64, G_ALLOC_AND_FREE); } frame = g_mem_chunk_alloc0 (xml_stack_frame_chunk); G_UNLOCK (xml_stack_frame_chunk); return frame; } static __inline__ void xml_stack_frame_free (XmlStackFrame *frame) { G_LOCK (xml_stack_frame_chunk); g_mem_chunk_free (xml_stack_frame_chunk, frame); G_UNLOCK (xml_stack_frame_chunk); } /* Push a stack frame for a value of the given type. */ static XmlStackFrame * xml_stack_push (GType type, XmlStackFrame *parent) { XmlStackFrame *frame; frame = xml_stack_frame_alloc0 (); frame->state = STATE_VALUE; frame->type = type; frame->parent = parent; if (type) g_value_init (&frame->value, type); return frame; } /* Push a stack frame for an object property. */ static XmlStackFrame * xml_stack_push_property (GParamSpec *param_spec, XmlStackFrame *parent) { GType type; parent->param_spec = param_spec; /* For GValueArray properties, we instantiate the element type. */ if (G_PARAM_SPEC_VALUE_TYPE (param_spec) == G_TYPE_VALUE_ARRAY) { GParamSpecValueArray *param_spec_value_array; g_return_val_if_fail (G_IS_PARAM_SPEC_VALUE_ARRAY (param_spec), NULL); param_spec_value_array = G_PARAM_SPEC_VALUE_ARRAY (param_spec); type = G_PARAM_SPEC_VALUE_TYPE (param_spec_value_array->element_spec); } else type = G_PARAM_SPEC_VALUE_TYPE (param_spec); return xml_stack_push (type, parent); } static void xml_stack_frame_destroy_one (XmlStackFrame *frame) { if (G_VALUE_TYPE (&frame->value)) g_value_unset (&frame->value); /* (param_spec is static) */ if (frame->properties) { GArray *properties = frame->properties; guint i; for (i = 0; i < properties->len; ++i) { /* (param->name is copy of static param_spec->name) */ GValue *value = & g_array_index (properties, GParameter, i).value; if (G_VALUE_TYPE (value)) g_value_unset (value); } g_array_free (properties, TRUE); } xml_stack_frame_free (frame); } static void xml_stack_frame_destroy_stack (XmlStackFrame *top) { while (top) { XmlStackFrame *parent = top->parent; xml_stack_frame_destroy_one (top); top = parent; } } /* * * GskXmlValueReader * */ struct _GskXmlValueReader { GMarkupParseContext *parse_context; GskGtypeLoader *type_loader; XmlStackFrame *stack; /* Position data for error reporting. */ char *filename; gint line_start, line_offset, char_offset; /* What type we must output. */ GType output_type; /* Callback to report output to client. */ GskXmlValueFunc value_callback; gpointer value_callback_data; GDestroyNotify value_callback_destroy; guint had_error : 1; }; #define GSK_XML_VALUE_READER(obj) ((GskXmlValueReader *) (obj)) /* * * Error handling * */ void gsk_xml_value_reader_set_error (GskXmlValueReader *reader, GError **error, gint error_code, const char *format, ...) { va_list args; char *message; gint line_no; gint char_no; reader->had_error = 1; va_start (args, format); message = g_strdup_vprintf (format, args); va_end (args); g_markup_parse_context_get_position (reader->parse_context, &line_no, &char_no); if (line_no == reader->line_start) char_no += reader->char_offset; line_no += reader->line_offset; if (reader->filename) g_set_error (error, GSK_G_ERROR_DOMAIN, error_code, "%s, line %d, character %d: %s", reader->filename, line_no, char_no, message); else g_set_error (error, GSK_G_ERROR_DOMAIN, error_code, "line %d, character %d: %s", line_no, char_no, message); g_free (message); } /* format describes what we got; add a description of what we expected. */ static void gsk_xml_value_reader_set_error_mismatch (GskXmlValueReader *reader, GError **error, gint error_code, const char *format, ...) { va_list args; XmlStackFrame *top = reader->stack; gchar *got, *expected; va_start (args, format); got = g_strdup_vprintf (format, args); va_end (args); g_return_if_fail (top); switch (top->state) { case STATE_VALUE: expected = g_strdup (" or text"); break; case STATE_OBJECT_BODY: expected = g_strdup_printf (", or ", g_type_name (top->type)); break; case STATE_PROPERTY_CLOSE: g_return_if_fail (top->param_spec); g_return_if_fail (top->param_spec->name); expected = g_strdup_printf ("", top->param_spec->name); break; case STATE_VALUE_TEXT: expected = g_strdup ("text"); break; case STATE_VALUE_CLOSE: expected = g_strdup_printf ("", g_type_name (top->type)); break; default: g_return_if_reached (); } gsk_xml_value_reader_set_error (reader, error, error_code, "got %s; expected %s", got, expected); g_free (expected); g_free (got); } /* A value has been instantiated and stored in stack->value, * by ... or text. * Deal with this value according to the parent stack frame. */ static void gsk_xml_value_reader_pop_value (GskXmlValueReader *reader) { XmlStackFrame *top = reader->stack; XmlStackFrame *parent = top->parent; GValue *value = &top->value; if (parent) { /* value is an object property. */ GArray *properties = parent->properties; GParamSpec *param_spec = parent->param_spec; GParameter *param; guint index; g_return_if_fail (parent->state == STATE_PROPERTY_CLOSE); g_return_if_fail (param_spec); if (properties == NULL) { properties = parent->properties = g_array_new (FALSE, FALSE, sizeof (GParameter)); } /* Special case for GValueArray properties. */ if (G_PARAM_SPEC_VALUE_TYPE (param_spec) == G_TYPE_VALUE_ARRAY) { GValueArray *value_array; /* Check whether the array has already been created. */ for (index = 0; index < properties->len; ++index) { param = & g_array_index (properties, GParameter, index); if (property_names_equal (param->name, param_spec->name)) { value_array = g_value_get_boxed (¶m->value); goto ADD; } } /* If not, create the array. */ value_array = g_value_array_new (1); g_array_set_size (properties, index + 1); param = & g_array_index (properties, GParameter, index); param->name = param_spec->name; memset (¶m->value, 0, sizeof (param->value)); g_value_init (¶m->value, G_TYPE_VALUE_ARRAY); g_value_set_boxed_take_ownership (¶m->value, value_array); ADD: g_value_array_append (value_array, value); } else { index = properties->len; g_array_set_size (properties, index + 1); param = & g_array_index (properties, GParameter, index); param->name = param_spec->name; g_value_move (¶m->value, value); } reader->stack = parent; } else { /* No parent stack frame; done with a top-level value. Invoke user * callback. */ if (reader->value_callback) (*reader->value_callback) (value, reader->value_callback_data); /* Replace top stack frame for next value. */ reader->stack = xml_stack_push (reader->output_type, NULL); } xml_stack_frame_destroy_one (top); } /* Text is either a simple type to be deserialized, or a * representation for g_value_transform. */ static gboolean instantiate_value_from_text (GskXmlValueReader *reader, const char *text, gsize text_len, GError **error) { XmlStackFrame *top = reader->stack; GValue *value = &top->value; GError *tmp_error = NULL; if (G_VALUE_TYPE (value) == G_TYPE_INVALID) { gsk_xml_value_reader_set_error (reader, error, 0, /* TODO: error_code */ "can't parse a value of " "unspecified type from text"); return FALSE; } if (!parse_text_value (value, text, text_len, &tmp_error)) { const char *msg = tmp_error ? tmp_error->message : "unknown error"; gsk_xml_value_reader_set_error (reader, error, 0, /* TODO: error_code */ "error parsing %s from text: %s", g_type_name (G_VALUE_TYPE (value)), msg); if (tmp_error) g_error_free (tmp_error); return FALSE; } return TRUE; } static void handle_start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { GskXmlValueReader *reader = GSK_XML_VALUE_READER (user_data); XmlStackFrame *top = reader->stack; (void) context; (void) attribute_names; (void) attribute_values; if (reader->had_error) return; g_return_if_fail (top); switch (top->state) { case STATE_VALUE: { /* Load the type and make sure it's allowed by the * current configuration. */ GError *tmp_error = NULL; GType type; type = gsk_gtype_loader_load_type (reader->type_loader, element_name, &tmp_error); if (type == G_TYPE_INVALID) { const char *msg = tmp_error ? tmp_error->message : "unknown error"; gsk_xml_value_reader_set_error (reader, error, 0, /* TODO: error_code */ "couldn't load type %s: %s", element_name, msg); g_error_free (tmp_error); return; } if (!gsk_gtype_loader_test_type (reader->type_loader, type)) { gsk_xml_value_reader_set_error (reader, error, 0, /* TODO: error_code */ "%s is not an allowed type", g_type_name (type)); return; } if (top->type) { /* XXX: do we really handle g_value_type_transformable? */ if (!(g_type_is_a (type, top->type) || g_value_type_transformable (type, top->type))) { gsk_xml_value_reader_set_error (reader, error, 0, /* TODO: error_code */ "%s is not a %s", g_type_name (type), g_type_name (top->type)); return; } } else { /* Only the top-level output type can be unspecified. */ g_return_if_fail (top->parent == NULL); g_value_init (&top->value, type); } /* Now scan for properties, if this is an object type, * or parse text if this is a fundamental type. */ top->type = type; if (g_type_is_a (type, G_TYPE_OBJECT)) top->state = STATE_OBJECT_BODY; else top->state = STATE_VALUE_TEXT; return; } case STATE_OBJECT_BODY: { /* Expecting . Is element_name a property? */ GObjectClass *klass; GParamSpec *param_spec; klass = G_OBJECT_CLASS (g_type_class_ref (top->type)); g_return_if_fail (klass); param_spec = g_object_class_find_property (klass, element_name); g_type_class_unref (klass); if (param_spec) { /* When we pop, we'll scan for . */ top->state = STATE_PROPERTY_CLOSE; reader->stack = xml_stack_push_property (param_spec, top); return; } /* element_name is not a property, error. */ gsk_xml_value_reader_set_error (reader, error, 0, /* TODO: error_code */ "%s is not a property of %s", element_name, g_type_name (top->type)); return; } default: break; } gsk_xml_value_reader_set_error_mismatch (reader, error, 0, /* TODO: error_code */ "tag <%s>", element_name); } static void handle_end_element (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { GskXmlValueReader *reader = GSK_XML_VALUE_READER (user_data); XmlStackFrame *top = reader->stack; (void) context; (void) error; if (reader->had_error) return; REDO: switch (top->state) { case STATE_OBJECT_BODY: { /* Expecting . */ const char *class_name; class_name = g_type_name (top->type); if (strcmp (element_name, class_name) == 0) { /* Try to construct the object. */ GParameter *parameters = NULL; int n_parameters = 0; GObject *object; if (top->properties) { parameters = (GParameter *) top->properties->data; n_parameters = top->properties->len; } object = g_object_newv (top->type, n_parameters, parameters); if (object == NULL) { gsk_xml_value_reader_set_error (reader, error, 0, /* TODO: error_code */ "error constructing a %s", class_name); return; } g_value_set_object (&top->value, object); /* value has referenced object */ g_object_unref (object); gsk_xml_value_reader_pop_value (reader); return; } } break; case STATE_PROPERTY_CLOSE: { /* Expecting . */ GArray *properties = top->properties; const char *property_name; g_return_if_fail (properties); property_name = g_array_index (properties, GParameter, properties->len - 1).name; g_return_if_fail (property_name); if (property_names_equal (element_name, property_name)) { /* Scan for next property. */ top->state = STATE_OBJECT_BODY; return; } } break; case STATE_VALUE: case STATE_VALUE_TEXT: { /* Might get a close tag when we're expecting a length 0 string. */ if (!instantiate_value_from_text (reader, "", 0, error)) return; if (top->state == STATE_VALUE) { gsk_xml_value_reader_pop_value (reader); top = reader->stack; } else top->state = STATE_VALUE_CLOSE; goto REDO; } break; case STATE_VALUE_CLOSE: { /* Expecting . */ const char *type_name; type_name = g_type_name (top->type); g_return_if_fail (type_name); if (strcmp (element_name, type_name) == 0) { gsk_xml_value_reader_pop_value (reader); return; } } break; default: break; } gsk_xml_value_reader_set_error_mismatch (reader, error, 0, /* TODO: error_code */ "tag ", element_name); } static void handle_text (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { GskXmlValueReader *reader = GSK_XML_VALUE_READER (user_data); XmlStackFrame *top = reader->stack; (void) context; g_return_if_fail (top); if (reader->had_error) return; /* Eat whitespace. */ while (text_len > 0) { char c = *text; if (!g_ascii_isspace (c)) break; --text_len; ++text; } if (text_len <= 0) return; if (top->state == STATE_VALUE || top->state == STATE_VALUE_TEXT) { if (instantiate_value_from_text (reader, text, text_len, error)) { if (top->state == STATE_VALUE) gsk_xml_value_reader_pop_value (reader); else top->state = STATE_VALUE_CLOSE; } return; } { char *terminated_text; terminated_text = g_strndup (text, text_len); gsk_xml_value_reader_set_error_mismatch (reader, error, 0, /* TODO: error_code */ "text '%s'", terminated_text); g_free (terminated_text); } } /* text is not nul-terminated. */ static void handle_passthrough (GMarkupParseContext *context, const gchar *passthrough_text, gsize text_len, gpointer user_data, GError **error) { GskXmlValueReader *reader = GSK_XML_VALUE_READER (user_data); XmlStackFrame *top = reader->stack; (void) context; if (reader->had_error) return; g_return_if_fail (top); /* Simply ignore any passthrough except CDATA. */ if (text_len < 12 || (strncmp (passthrough_text, "", 3) != 0)) return; passthrough_text += 9; text_len -= 12; /* TODO: perhaps CDATA should be merged with plain text blocks? */ if (top->state == STATE_VALUE || top->state == STATE_VALUE_TEXT) { if (instantiate_value_from_text (reader, passthrough_text, text_len, error)) { if (top->state == STATE_VALUE) gsk_xml_value_reader_pop_value (reader); else top->state = STATE_VALUE_CLOSE; } return; } { char *terminated_text; terminated_text = g_strndup (passthrough_text, text_len); gsk_xml_value_reader_set_error_mismatch (reader, error, 0, /* TODO: error_code */ "text '%s'", terminated_text); g_free (terminated_text); } } /* Create the GMarkupParseContext and the stack. */ static void gsk_xml_value_reader_create_parser (GskXmlValueReader *reader) { static const GMarkupParser g_markup_parser = { handle_start_element, handle_end_element, handle_text, handle_passthrough, NULL, /* error */ }; g_return_if_fail (reader->parse_context == NULL); reader->parse_context = g_markup_parse_context_new (&g_markup_parser, 0, /* flags */ reader, /* user_data */ NULL); /* user_data_dnotify */ g_return_if_fail (reader->stack == NULL); reader->stack = xml_stack_push (reader->output_type, NULL); } /* * * Public interface. * */ GskXmlValueReader * gsk_xml_value_reader_new (GskGtypeLoader *type_loader, GType output_type, GskXmlValueFunc value_func, gpointer value_func_data, GDestroyNotify value_func_destroy) { GskXmlValueReader *reader; g_return_val_if_fail (type_loader, NULL); reader = g_new0 (GskXmlValueReader, 1); reader->type_loader = type_loader; gsk_gtype_loader_ref (type_loader); reader->output_type = output_type; g_return_val_if_fail (output_type == G_TYPE_INVALID || g_type_name (output_type), NULL); reader->value_callback = value_func; reader->value_callback_data = value_func_data; reader->value_callback_destroy = value_func_destroy; return reader; } void gsk_xml_value_reader_free (GskXmlValueReader *reader) { if (reader->value_callback_destroy) (*reader->value_callback_destroy) (reader->value_callback_data); if (reader->parse_context) { /* XXX: this isn't reentrant... */ g_markup_parse_context_free (reader->parse_context); reader->parse_context = NULL; } xml_stack_frame_destroy_stack (reader->stack); if (reader->type_loader) { gsk_gtype_loader_unref (reader->type_loader); reader->type_loader = NULL; } g_free (reader); } gboolean gsk_xml_value_reader_input (GskXmlValueReader *reader, const char *input, guint len, GError **error) { const char *start = input; if (reader->had_error) return FALSE; if (reader->parse_context == NULL) { /* Only create parser if we get non-whitespace. */ for (;;) { if (len == 0) return TRUE; if (!g_ascii_isspace (*start)) break; if (*start == '\n') { ++reader->line_offset; reader->char_offset = 0; } else ++reader->char_offset; ++start; --len; } gsk_xml_value_reader_create_parser (reader); } /* Feed the data to the parser. */ return g_markup_parse_context_parse (reader->parse_context, start, len, error); } void gsk_xml_value_reader_set_pos (GskXmlValueReader *reader, const char *filename, gint line_no, gint char_no) { gint parser_line_no, parser_char_no; if (reader->filename) g_free (reader->filename); reader->filename = filename ? g_strdup (filename) : NULL; if (reader->parse_context == NULL) gsk_xml_value_reader_create_parser (reader); g_markup_parse_context_get_position (reader->parse_context, &parser_line_no, &parser_char_no); reader->line_start = parser_line_no; reader->line_offset = line_no - parser_line_no; reader->char_offset = char_no - parser_char_no; } gboolean gsk_xml_value_reader_had_error (GskXmlValueReader *reader) { return reader->had_error; } /* * * gsk_load_object_from_xml_file * */ /* GskXmlValueFunc */ static void set_object_ptr (const GValue *value, gpointer user_data) { *((GObject **) user_data) = g_value_dup_object (value); } GObject * gsk_load_object_from_xml_file (const char *path, GskGtypeLoader *type_loader, GType object_type, GError **error) { GskXmlValueReader *xml_value_reader = NULL; char *file_contents = NULL; gsize file_contents_len; GObject *object = NULL; if (!g_file_get_contents (path, &file_contents, &file_contents_len, error)) goto FAIL; g_return_val_if_fail (file_contents, NULL); xml_value_reader = gsk_xml_value_reader_new (type_loader, object_type, set_object_ptr, &object, NULL); g_return_val_if_fail (xml_value_reader, NULL); if (!gsk_xml_value_reader_input (xml_value_reader, file_contents, file_contents_len, error)) goto FAIL; if (object == NULL || !g_type_is_a (G_OBJECT_TYPE (object), object_type)) goto FAIL; gsk_xml_value_reader_free (xml_value_reader); g_free (file_contents); return object; FAIL: if (object) g_object_unref (object); if (file_contents) g_free (file_contents); if (xml_value_reader) gsk_xml_value_reader_free (xml_value_reader); return NULL; }