/* * Copyright © 2003 Red Hat, Inc. * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation, and that the name of Red Hat not be used in advertising or * publicity pertaining to distribution of the software without specific, * written prior permission. Red Hat makes no representations about the * suitability of this software for any purpose. It is provided "as is" * without express or implied warranty. * * RED HAT DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL RED HAT * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * Author: Owen Taylor, Red Hat, Inc. */ #include #include #include #include "themefile.h" typedef struct _ParseData ParseData; typedef enum { OUTSIDE, IN_THEME, IN_SOURCE, IN_IMAGE, IN_ALIAS, IN_LAYOUT, IN_ROW, IN_CURSOR, IN_FRAME, IN_ICON, IN_DISPLAYNAME, IN_LOCALE, } ParseState; struct _ParseData { const char *image_dir; ThemeFile *theme; ParseState state; gboolean seen_theme; gboolean seen_layout; int row; int column; ThemeCursor *current_cursor; ThemeIcon *current_icon; ThemeAlias *current_alias; }; static gboolean expect_tag (GMarkupParseContext *context, const gchar *element_name, const gchar *expected_name, /* Null for expected empty */ GError **error) { if (!expected_name || strcmp (element_name, expected_name) != 0) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Unexpected tag '%s'", element_name); return FALSE; } return TRUE; } static gboolean extract_attrs (GMarkupParseContext *context, const gchar **attribute_names, const gchar **attribute_values, GError **error, ...) { va_list vap; const char *name; gboolean *attr_map; gboolean nattrs = 0; int i; for (i = 0; attribute_names[i]; i++) nattrs++; attr_map = g_new0 (gboolean, nattrs); va_start (vap, error); name = va_arg (vap, const char *); while (name) { gboolean mandatory = va_arg (vap, gboolean); const char **loc = va_arg (vap, const char **); gboolean found = FALSE; for (i = 0; attribute_names[i]; i++) { if (!attr_map[i] && strcmp (attribute_names[i], name) == 0) { if (found) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Duplicate attribute '%s'", name); return FALSE; } *loc = attribute_values[i]; found = TRUE; attr_map[i] = TRUE; } } if (!found && mandatory) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Missing attribute '%s'", name); return FALSE; } else if (!found) *loc = NULL; name = va_arg (vap, const char *); } for (i = 0; i < nattrs; i++) if (!attr_map[i]) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, "Unknown attribute '%s'", attribute_names[i]); return FALSE; } return TRUE; } static gboolean get_int (const char *str, int *result) { long val; char *p; val = strtol (str, &p, 0); if (*str == '\0' || *p != '\0' || val < G_MININT || val > G_MAXINT) return FALSE; *result = val; return TRUE; } /* Strip out duplicate internal slashes and trailing slashes */ static char * canonicalize_dir (const char *dir) { char *p, *q; char *result; gboolean last_was_slash = FALSE; if (!dir) return NULL; else result = g_strdup (dir); for (p = result, q = result; *p; *p++) { if (!((last_was_slash && p[0] == '/') || (p[0] == '/' && p[1] == '\0'))) *q++ = *p; last_was_slash = (*p == '/'); } return result; } static void add_source (ParseData *parse_data, int size, int gridsize, int margin, int spacing, const char *sizedir) { ThemeSource *source = g_new0 (ThemeSource, 1); source->size = size; source->gridsize = gridsize; source->margin = margin; source->spacing = spacing; source->sizedir = canonicalize_dir (sizedir); parse_data->theme->sources = g_slist_prepend (parse_data->theme->sources, source); } static void add_alias (ParseData *parse_data, const char *name, const char *target, const char *typedir) { ThemeAlias *alias = g_new0 (ThemeAlias, 1); alias->name = g_strdup (name); alias->target = g_strdup (target); alias->typedir = canonicalize_dir (typedir); g_hash_table_insert (parse_data->theme->aliases, alias->name, alias); parse_data->current_alias = alias; } static gboolean add_image (ParseData *parse_data, const char *file, int use, GError **error) { GdkPixbuf *pixbuf; ThemeSource *source; ThemeImage *image; char *pathname; if (parse_data->image_dir) pathname = g_build_filename (parse_data->image_dir, file, NULL); else pathname = g_strdup (file); pixbuf = gdk_pixbuf_new_from_file (pathname, error); g_free (pathname); if (!pixbuf) return FALSE; source = parse_data->theme->sources->data; image = g_new0 (ThemeImage, 1); image->image = pixbuf; image->use = use; source->images = g_slist_prepend (source->images, image); return TRUE; } static void add_cursor (ParseData *parse_data, const char *name) { ThemeCursor *cursor = g_new0 (ThemeCursor, 1); cursor->name = g_strdup (name); cursor->location.row = parse_data->row; cursor->location.column = parse_data->column; parse_data->current_cursor = cursor; g_hash_table_insert (parse_data->theme->cursors, cursor->name, cursor); } static void add_icon (ParseData *parse_data, const char *name, const char *typedir) { ThemeIcon *icon = g_new0 (ThemeIcon, 1); icon->name = g_strdup (name); icon->typedir = canonicalize_dir (typedir); icon->location.row = parse_data->row; icon->location.column = parse_data->column; parse_data->current_icon = icon; g_hash_table_insert (parse_data->theme->icons, icon->name, icon); } static void add_frame_config (ParseData *parse_data, int delay) { ThemeCursor *cursor = parse_data->current_cursor; ThemeFrameConfig *frame_config = g_new0 (ThemeFrameConfig, 1); frame_config->delay = delay; cursor->frame_configs = g_slist_prepend (cursor->frame_configs, frame_config); } static void add_display_name (ParseData *parse_data, const char *lang, const char *str) { ThemeDisplayName *display_name = g_new0 (ThemeDisplayName, 1); display_name->lang = g_strdup (lang); display_name->str = g_strdup (str); if (parse_data->current_icon) { ThemeIcon *icon = parse_data->current_icon; icon->display_names = g_slist_append (icon->display_names, display_name); } else { ThemeAlias *alias = parse_data->current_alias; alias->display_names = g_slist_append (alias->display_names, display_name); } } /* The primary goal of validate_name() and validate_sizedir() * is to keep the output of icon-slicer within the specified icon * directory, and avoid writing elsewhere on the filesystem * * We don't try to worry much about what is *sensible* for names * as long as they won't break our operation. */ static gboolean validate_name (const char *name, const char *whatisit, GError **error) { const char *p; gboolean valid = TRUE; if (!name[0] || name[0] == '.') valid = FALSE; for (p = name; *p; p++) { if ((guchar) *p >= 0x80 || g_ascii_iscntrl(*p) || g_ascii_isspace(*p) || *p == '/') valid = FALSE; } if (!valid) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "invalid %s '%s'", whatisit ? whatisit : "name", name); } return valid; } static gboolean validate_dir (const char *dir, const char *attribute_name, GError **error) { char **elements; char **element; gboolean valid; if (!dir[0] || dir[0] == '/') { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "invalid %s value '%s'", attribute_name, dir); return FALSE; } elements = g_strsplit (dir, "/", -1); valid = TRUE; for (element = elements; *element; element++) { if (!validate_name (*element, "directory component", error)) valid = FALSE; } g_strfreev (elements); return valid; } /* Called for open tags */ static void theme_file_start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { ParseData *parse_data = user_data; switch (parse_data->state) { case OUTSIDE: { const char *name; const char *typedir; if (!expect_tag (context, element_name, "theme", error)) return; if (parse_data->seen_theme) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Multiple occurrences of "); return; } parse_data->seen_theme = TRUE; parse_data->state = IN_THEME; if (!extract_attrs (context, attribute_names, attribute_values, error, "name", TRUE, &name, "typedir", FALSE, &typedir, NULL)) return; if (!validate_name (name, NULL, error) || (typedir && !validate_dir (typedir, "typedir", error))) return; parse_data->theme->name = g_strdup (name); parse_data->theme->typedir = canonicalize_dir (typedir); } break; case IN_THEME: if (strcmp (element_name, "source") == 0) { const char *size_str; const char *gridsize_str; const char *margin_str; const char *spacing_str; const char *sizedir; int size, gridsize, margin, spacing; parse_data->state = IN_SOURCE; if (!extract_attrs (context, attribute_names, attribute_values, error, "size", TRUE, &size_str, "gridsize", FALSE, &gridsize_str, "margin", FALSE, &margin_str, "spacing", FALSE, &spacing_str, "sizedir", FALSE, &sizedir, NULL)) return; if (!get_int (size_str, &size) || size < 0) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Invalid size %s", size_str); return; } if (gridsize_str) { if (!get_int (gridsize_str, &gridsize) || size < 0) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Invalid gridsize %s", gridsize_str); return; } } else gridsize = size; if (margin_str) { if (!get_int (margin_str, &margin) || size < 0) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Invalid margin %s", margin_str); return; } } else margin = 0; if (spacing_str) { if (!get_int (spacing_str, &spacing) || size < 0) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Invalid spacing %s", spacing_str); return; } } else spacing = 0; if (sizedir && !validate_dir (sizedir, "sizedir", error)) return; add_source (parse_data, size, gridsize, margin, spacing, sizedir); } else if (strcmp (element_name, "alias") == 0) { const char *name; const char *target; const char *typedir; parse_data->state = IN_ALIAS; if (!extract_attrs (context, attribute_names, attribute_values, error, "name", TRUE, &name, "target", FALSE, &target, "typedir", FALSE, &typedir, NULL)) return; if (!validate_name (name, NULL, error) || !validate_name (target, NULL, error) || (typedir && !validate_dir (typedir, "typedir", error))) return; add_alias (parse_data, name, target, typedir); } else if (strcmp (element_name, "layout") == 0) { if (parse_data->seen_layout) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Multiple occurrences of "); return; } parse_data->seen_layout = TRUE; parse_data->state = IN_LAYOUT; if (!extract_attrs (context, attribute_names, attribute_values, error, NULL)) return; } else expect_tag (context, element_name, NULL, error); break; case IN_SOURCE: { const char *file; const char *use_str; int use; if (!expect_tag (context, element_name, "image", error)) return; parse_data->state = IN_IMAGE; if (!extract_attrs (context, attribute_names, attribute_values, error, "file", TRUE, &file, "use", FALSE, &use_str, NULL)) return; if (!use_str) use = 0; else if (strcmp (use_str, "hotspot") == 0) use = THEME_SOURCE_USE_HOTSPOT; else if (strcmp (use_str, "embeddedrect") == 0) use = THEME_SOURCE_USE_EMBEDDED_RECT; else if (strcmp (use_str, "attachpoints") == 0) use = THEME_SOURCE_USE_ATTACH_POINTS; else { if (!get_int (use_str, &use) || use < 0) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Invalid use value '%s'", use_str); return; } } add_image (parse_data, file, use, error); } break; case IN_LAYOUT: if (!expect_tag (context, element_name, "row", error)) return; parse_data->state = IN_ROW; parse_data->column = 0; break; case IN_ROW: if (strcmp (element_name, "cursor") == 0) { const char *name; parse_data->state = IN_CURSOR; if (!extract_attrs (context, attribute_names, attribute_values, error, "name", TRUE, &name, NULL)) return; if (!validate_name (name, NULL, error)) return; add_cursor (parse_data, name); } else if (strcmp (element_name, "icon") == 0) { const char *name; const char *typedir; parse_data->state = IN_ICON; if (!extract_attrs (context, attribute_names, attribute_values, error, "name", TRUE, &name, "typedir", FALSE, &typedir, NULL)) return; if (!validate_name (name, NULL, error) || (typedir && !validate_dir (typedir, "typedir", error))) return; add_icon (parse_data, name, typedir); } else expect_tag (context, element_name, NULL, error); break; case IN_CURSOR: { const char *delay_str = NULL; int delay = -1; if (!expect_tag (context, element_name, "frame", error)) return; parse_data->state = IN_FRAME; if (!extract_attrs (context, attribute_names, attribute_values, error, "delay", FALSE, &delay_str, NULL)) return; if (delay_str) { if (!get_int (delay_str, &delay) || delay < 0) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Invalid delay value '%s'", delay_str); return; } } add_frame_config (parse_data, delay); } break; case IN_ICON: { const char *str; if (!expect_tag (context, element_name, "displayname", error)) return; parse_data->state = IN_DISPLAYNAME; if (!extract_attrs (context, attribute_names, attribute_values, error, "str", TRUE, &str, NULL)) return; add_display_name (parse_data, NULL, str); break; } case IN_DISPLAYNAME: { const char *lang; const char *str; if (!expect_tag (context, element_name, "locale", error)) return; parse_data->state = IN_LOCALE; if (!extract_attrs (context, attribute_names, attribute_values, error, "lang", TRUE, &lang, "str", TRUE, &str, NULL)) return; add_display_name (parse_data, lang, str); break; } case IN_ALIAS: { const char *str; if (!expect_tag (context, element_name, "displayname", error)) return; parse_data->state = IN_DISPLAYNAME; if (!extract_attrs (context, attribute_names, attribute_values, error, "str", TRUE, &str, NULL)) return; add_display_name (parse_data, NULL, str); break; } break; case IN_IMAGE: case IN_LOCALE: case IN_FRAME: expect_tag (context, element_name, NULL, error); break; } } /* Called for close tags */ static void theme_file_end_element (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { ParseData *parse_data = user_data; switch (parse_data->state) { case OUTSIDE: g_assert_not_reached (); break; case IN_THEME: parse_data->state = OUTSIDE; parse_data->theme->sources = g_slist_reverse (parse_data->theme->sources); break; case IN_SOURCE: { ThemeSource *source = parse_data->theme->sources->data; source->images = g_slist_reverse (source->images); parse_data->state = IN_THEME; } break; case IN_ALIAS: parse_data->current_alias = NULL; parse_data->state = IN_THEME; break; case IN_IMAGE: parse_data->state = IN_SOURCE; break; case IN_LAYOUT: parse_data->state = IN_THEME; break; case IN_ROW: parse_data->state = IN_LAYOUT; parse_data->row++; break; case IN_CURSOR: { ThemeCursor *cursor = parse_data->current_cursor; if (!cursor->frame_configs) add_frame_config (parse_data, -1); cursor->frame_configs = g_slist_reverse (cursor->frame_configs); parse_data->state = IN_ROW; parse_data->column++; parse_data->current_cursor = NULL; } break; case IN_FRAME: parse_data->state = IN_CURSOR; break; case IN_ICON: parse_data->state = IN_ROW; parse_data->column++; parse_data->current_icon = NULL; break; case IN_DISPLAYNAME: if (parse_data->current_icon) parse_data->state = IN_ICON; else parse_data->state = IN_ALIAS; break; case IN_LOCALE: parse_data->state = IN_DISPLAYNAME; break; } } /* Called for character data */ /* text is not nul-terminated */ static void theme_file_text (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { int i; for (i = 0; i < text_len; i++) if (!g_ascii_isspace (text[i])) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Unexpected text in theme file"); return; } } /* Called for strings that should be re-saved verbatim in this same * position, but are not otherwise interpretable. At the moment * this includes comments and processing instructions. */ /* text is not nul-terminated. */ static void theme_file_passthrough (GMarkupParseContext *context, const gchar *passthrough_text, gsize text_len, gpointer user_data, GError **error) { /* do nothing */ } /* Called on error, including one set by other * methods in the vtable. The GError should not be freed. */ static void theme_file_error (GMarkupParseContext *context, GError *error, gpointer user_data) { } static GMarkupParser theme_file_parse = { theme_file_start_element, theme_file_end_element, theme_file_text, theme_file_passthrough, theme_file_error }; static void theme_image_free (ThemeImage *image) { g_object_unref (image->image); g_free (image); } static void theme_source_free (ThemeSource *source) { g_free (source->sizedir); g_slist_foreach (source->images, (GFunc)theme_image_free, NULL); g_slist_free (source->images); g_free (source); } static void theme_frame_config_free (ThemeFrameConfig *frame_config) { g_free (frame_config); } static void theme_frame_free (ThemeFrame *frame) { g_object_unref (frame->image); g_free (frame); } static void cursor_free_foreach (gpointer key, gpointer value, gpointer data) { ThemeCursor *cursor = value; g_free (cursor->name); g_slist_foreach (cursor->frame_configs, (GFunc)theme_frame_config_free, NULL); g_slist_free (cursor->frame_configs); g_slist_foreach (cursor->frames, (GFunc)theme_frame_free, NULL); g_slist_free (cursor->frames); g_free (cursor); } static void theme_icon_instance_free (ThemeIconInstance *instance) { g_slist_foreach (instance->attach_points, (GFunc)g_free, NULL); g_slist_free (instance->attach_points); g_free (instance->embedded_rect); g_object_unref (instance->image); g_free (instance); } static void theme_display_name_free (ThemeDisplayName *display_name) { g_free (display_name->lang); g_free (display_name->str); g_free (display_name); } static void icon_free_foreach (gpointer key, gpointer value, gpointer data) { ThemeIcon *icon = value; g_free (icon->name); g_free (icon->typedir); g_slist_foreach (icon->instances, (GFunc)theme_icon_instance_free, NULL); g_slist_free (icon->instances); g_slist_foreach (icon->display_names, (GFunc)theme_display_name_free, NULL); g_slist_free (icon->display_names); g_free (icon); } static void alias_free_foreach (gpointer key, gpointer value, gpointer data) { ThemeAlias *alias = value; g_free (alias->name); g_free (alias->target); g_free (alias->typedir); g_slist_foreach (alias->display_names, (GFunc)theme_display_name_free, NULL); g_slist_free (alias->display_names); g_free (alias); } void theme_file_free (ThemeFile *theme) { g_free (theme->name); g_free (theme->typedir); g_slist_foreach (theme->sources, (GFunc)theme_source_free, NULL); g_slist_free (theme->sources); g_hash_table_foreach (theme->cursors, cursor_free_foreach, NULL); g_hash_table_destroy (theme->cursors); g_hash_table_foreach (theme->icons, icon_free_foreach, NULL); g_hash_table_destroy (theme->icons); g_hash_table_foreach (theme->aliases, alias_free_foreach, NULL); g_hash_table_destroy (theme->aliases); g_free (theme); } ThemeFile * theme_file_read (const char *filename, const char *image_dir) { ParseData parse_data; GMarkupParseContext *context; GError *error = NULL; char *text; gboolean have_error = FALSE; size_t len; if (!g_file_get_contents (filename, &text, &len, &error)) { g_printerr ("Cannot read theme definition file: %s\n", error->message); g_error_free (error); return NULL; } parse_data.image_dir = image_dir; parse_data.theme = g_new0 (ThemeFile, 1); parse_data.theme->cursors = g_hash_table_new (g_str_hash, g_str_equal); parse_data.theme->icons = g_hash_table_new (g_str_hash, g_str_equal); parse_data.theme->aliases = g_hash_table_new (g_str_hash, g_str_equal); parse_data.state = OUTSIDE; parse_data.seen_theme = FALSE; parse_data.seen_layout = FALSE; parse_data.row = 0; parse_data.column = 0; context = g_markup_parse_context_new (&theme_file_parse, 0, &parse_data, NULL); if (!g_markup_parse_context_parse (context, text, len, &error) || !g_markup_parse_context_end_parse (context, &error)) { g_printerr ("Error parsing theme definition file: %s\n", error->message); have_error = TRUE; g_error_free (error); } else if (!parse_data.seen_theme) { g_printerr ("Did not find element in theme file\n"); have_error = TRUE; } else if (!parse_data.seen_layout) { g_printerr ("Did not find element in theme file\n"); have_error = TRUE; } else if (!parse_data.theme->sources) { g_printerr ("No element in theme file\n"); have_error = TRUE; } g_markup_parse_context_free (context); if (!have_error) return parse_data.theme; else { theme_file_free (parse_data.theme); return NULL; } }