#include #include #include #include #include #include #include "addressentry.h" /* Basic handling of AddressEntry structures */ AddressEntry * address_entry_new (void) { AddressEntry *entry; entry = g_new0 (AddressEntry, 1); entry->ref_count = 1; entry->firstname = g_strdup (""); entry->lastname = g_strdup (""); entry->month = 1; entry->day = 1; entry->address = g_strdup (""); return entry; } AddressEntry * address_entry_ref (AddressEntry *entry) { entry->ref_count++; return entry; } void address_entry_unref (AddressEntry *entry) { entry->ref_count--; if (entry->ref_count == 0) { g_free (entry->firstname); g_free (entry->lastname); g_free (entry); } } void address_entry_set_firstname (AddressEntry *entry, const char *firstname) { g_free (entry->firstname); entry->firstname = g_strdup (firstname); } void address_entry_set_lastname (AddressEntry *entry, const char *lastname) { g_free (entry->lastname); entry->lastname = g_strdup (lastname); } void address_entry_set_address (AddressEntry *entry, const char *address) { g_free (entry->address); entry->address = g_strdup (address); } GType address_entry_get_type (void) { static GType our_type = 0; if (our_type == 0) our_type = g_boxed_type_register_static ("AddressEntry", (GBoxedCopyFunc) address_entry_ref, (GBoxedFreeFunc) address_entry_unref); return our_type; } /* * Support for loading lists of addressbook entries from an XML-type format */ /* Something like the next couple of convenience functions probably will make * it's way into GLib eventually, they make using GMarkup considerably * easier to use. */ static int expect_tag (GMarkupParseContext *context, const gchar *element_name, GError **error, ...) { va_list vap; const char *expected; int n_expected = 0; va_start (vap, error); expected = va_arg (vap, const char *); while (expected) { int value = va_arg (vap, int); n_expected++; if (strcmp (expected, element_name) == 0) return value; expected = va_arg (vap, const char *); } va_end (vap); if (n_expected == 0) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Unexpected tag '%s', no tags expected", element_name); } else { GString *tag_string = g_string_new (NULL); va_start (vap, error); expected = va_arg (vap, const char *); while (expected) { va_arg (vap, int); if (tag_string->len) g_string_append (tag_string, ", "); g_string_append (tag_string, expected); expected = va_arg (vap, const char *); } va_end (vap); if (n_expected == 1) g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Unexpected tag '%s', expected '%s'", element_name, tag_string->str); else g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Unexpected tag '%s', expected one of: %s", element_name, tag_string->str); g_string_free (tag_string, TRUE); } return 0; } 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; } 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 check_once (gboolean *once_var, const char *name, GError **error) { if (*once_var) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Multiple occurrences of <%s>", name); return FALSE; } *once_var = TRUE; return TRUE; } typedef struct ParseInfo ParseInfo; typedef enum { INITIAL, IN_BOOK, IN_ENTRY, IN_NAME, IN_BIRTHDAY, IN_ADDRESS, FINAL } ParseState; typedef enum { UNKNOWN = 0, BOOK, ENTRY, NAME, BIRTHDAY, ADDRESS } ParseTag; struct ParseInfo { ParseState state; AddressEntry *current; GString *address_string; GList *entries; gboolean seen_name; gboolean seen_birthday; gboolean seen_address; }; 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; } /* Called for open tags */ static void on_start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { ParseInfo *info = user_data; ParseTag tag; switch (info->state) { case INITIAL: if (expect_tag (context, element_name, error, "book", BOOK, NULL) && extract_attrs (context, attribute_names, attribute_values, error, NULL)) info->state = IN_BOOK; break; case IN_BOOK: if (expect_tag (context, element_name, error, "entry", ENTRY, NULL) && extract_attrs (context, attribute_names, attribute_values, error, NULL)) { info->current = address_entry_new (); info->entries = g_list_prepend (info->entries, info->current); info->state = IN_ENTRY; } break; case IN_ENTRY: tag = expect_tag (context, element_name, error, "name", NAME, "birthday", BIRTHDAY, "address", ADDRESS, NULL); switch (tag) { case NAME: { const char *firstname; const char *lastname; if (check_once (&info->seen_name, "name", error) && extract_attrs (context, attribute_names, attribute_values, error, "firstname", TRUE, &firstname, "lastname", TRUE, &lastname, NULL)) { info->state = IN_NAME; address_entry_set_firstname (info->current, firstname); address_entry_set_lastname (info->current, lastname); } break; } case BIRTHDAY: { const char *month; const char *day; if (check_once (&info->seen_birthday, "birthday", error) && extract_attrs (context, attribute_names, attribute_values, error, "month", TRUE, &month, "day", TRUE, &day, NULL)) { info->state = IN_BIRTHDAY; if (!get_int (month, &info->current->day) || info->current->month < 1 || info->current->month > 12) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Bad month value '%s'", month); } if (!get_int (day, &info->current->day) || info->current->day < 1 || info->current->day > 31) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "Bad day value '%s'", day); } } } break; case ADDRESS: if (check_once (&info->seen_address, "address", error) && extract_attrs (context, attribute_names, attribute_values, error, NULL)) { info->state = IN_ADDRESS; info->address_string = g_string_new (""); } break; default: break; } break; case IN_NAME: expect_tag (context, element_name, error, NULL); break; case IN_BIRTHDAY: expect_tag (context, element_name, error, NULL); break; case IN_ADDRESS: expect_tag (context, element_name, error, NULL); break; case FINAL: expect_tag (context, element_name, error, NULL); break; } } /* Called for close tags */ static void on_end_element (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { ParseInfo *info = user_data; switch (info->state) { case INITIAL: g_assert_not_reached (); break; case IN_BOOK: info->state = FINAL; break; case IN_ENTRY: info->state = IN_BOOK; info->current = NULL; info->seen_name = FALSE; info->seen_birthday = FALSE; info->seen_address = FALSE; break; case IN_NAME: info->state = IN_ENTRY; break; case IN_BIRTHDAY: info->state = IN_ENTRY; break; case IN_ADDRESS: address_entry_set_address (info->current, info->address_string->str); g_string_free (info->address_string, FALSE); info->address_string = NULL; info->state = IN_ENTRY; break; case FINAL: g_assert_not_reached (); break; } } /* Called for character data */ /* text is not nul-terminated */ static void on_text (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { ParseInfo *info = user_data; int i; switch (info->state) { case IN_ADDRESS: g_string_append_len (info->address_string, text, text_len); break; case INITIAL: case IN_BOOK: case IN_ENTRY: case IN_NAME: case IN_BIRTHDAY: case FINAL: 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; } break; } } static const GMarkupParser parser = { on_start_element, on_end_element, on_text, NULL, NULL }; gboolean address_entry_read_file (const gchar *filename, GList **entries_out, GError **error) { ParseInfo info; GMarkupParseContext *context; char *contents; gsize len; gboolean result; info.state = INITIAL; info.entries = NULL; info.address_string = NULL; info.current = NULL; info.seen_name = FALSE; info.seen_birthday = FALSE; info.seen_address = FALSE; context = g_markup_parse_context_new (&parser, 0, &info, NULL); if (!g_file_get_contents (filename, &contents, &len, error)) return FALSE; result = g_markup_parse_context_parse (context, contents, len, error); if (result) *entries_out = g_list_reverse (info.entries); else { g_list_foreach (info.entries, (GFunc)address_entry_unref, NULL); g_list_free (info.entries); if (info.address_string) g_string_free (info.address_string, TRUE); } g_markup_parse_context_free (context); g_free (contents); return result; } /* Write out a list of entries in the format that the above * parser reads back in. */ gboolean address_entry_write_file (const gchar *filename, GList *entries, GError **error) { char *tmp_filename = g_strconcat (filename, ".new", NULL); FILE *file; gboolean result = FALSE; gboolean opened_file = FALSE; GList *l; file = fopen (tmp_filename, "w"); if (!file) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), "Cannot open temporary file '%s': %s", tmp_filename, g_strerror (errno)); goto error; } opened_file = TRUE; if (fprintf (file, "\n") < 0) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), "Error writing to temporary file '%s': %s", tmp_filename, g_strerror (errno)); goto error; } for (l = entries; l; l = l->next) { AddressEntry *entry = l->data; if (fprintf (file, "\n" "\n" "\n" "
%s
\n" "
\n", entry->firstname ? entry->firstname : "", entry->lastname ? entry->lastname : "", entry->month, entry->day, entry->address ? entry->address: "") < 0) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), "Error writing to temporary file '%s': %s", tmp_filename, g_strerror (errno)); goto error; } } if (fprintf (file, "
\n") < 0) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), "Error writing to temporary file '%s': %s", tmp_filename, g_strerror (errno)); goto error; } if (fclose (file) == EOF) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), "Error writing to temporary file '%s': %s", tmp_filename, g_strerror (errno)); goto error; } if (rename (tmp_filename, filename) == -1) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), "Error renaming '%s' to '%s': %s", tmp_filename, filename, g_strerror (errno)); } else result = TRUE; error: if (!result && opened_file) unlink (tmp_filename); g_free (tmp_filename); return result; }