#include <stdlib.h> #include <stdio.h> #include <string.h> #include <stdbool.h> #include <stdint.h> #include <libxml/tree.h> #include <libxml/parser.h> #include <libxml/parserInternals.h> #ifdef _WIN32 #include <mysql.h> #else #include <mysql/mysql.h> #endif #include "hashtable.h" #include "string_buf.h" #define X(type, name) type, #define Y(type, name) X(type, name) enum field_type { #include "field_types.c" }; const enum field_type field_type_ids[] = { #include "field_types.c" }; #undef X #undef Y #define X(type, name) [type] = #type, #define Y(type, name) X(type, name) static const char *const field_enum_names[] = { #include "field_types.c" }; #undef X #undef Y #define X(type, name) [type] = name, #define Y(type, name) X(type, name) static const char *const field_names[] = { #include "field_types.c" }; #undef X #undef Y static inline bool is_rel_type(enum field_type type) { return type == ONE_TO_ONE_REL || type == MANY_TO_ONE_REL || type == MANY_TO_MANY_REL || type == BROKEN_TO_ONE_REL || type == BROKEN_TO_MANY_REL; } static inline bool is_text_type(enum field_type type) { return type == CHAR_FIELD || type == FILE_FIELD || type == TEXT_FIELD; } /* All keys and values are static, ht_destroy() is enough to free this. */ static int make_field_names_ht(hashtable_t *ht) { int ret; int i; ret = ht_string_init(ht); if (ret) return ret; for (i = 0; i < sizeof(field_type_ids) / sizeof(*field_type_ids); i++) { ret = ht_add(ht, field_names[i], &field_type_ids[i]); if (ret) { ht_destroy(ht); break; } } return ret; } inline static void *create_struct_with_string(size_t minimum_size, size_t char_field_offset, const char *string) { size_t string_size = strlen(string) + 1; size_t dynamic_size = char_field_offset + string_size; char *mem; mem = malloc(dynamic_size > minimum_size ? dynamic_size : minimum_size); if (mem) memcpy(mem + char_field_offset, string, string_size); return mem; } #define STRUCT_WITH_STRING_CREATOR(fun_name, type, char_field) \ static type *fun_name(const char *string) \ { \ return (type*) create_struct_with_string \ (sizeof(type), \ (size_t) ((type*) 0)->char_field, \ string); \ } struct field { struct field *next; union field_data { size_t max_length; char *target_table; /* NULL means mtm table creation failure */ bool array_has_data; } spec_data; uint8_t type; char name[1]; }; STRUCT_WITH_STRING_CREATOR(create_field, struct field, name) struct table { struct field *fields; size_t all_rows; size_t inserted_rows; size_t insertion_errors; bool creation_error; char name[1]; }; STRUCT_WITH_STRING_CREATOR(create_table, struct table, name) struct restore { size_t errors_count; enum { PASS_1 = 1, PASS_2 = 2 } pass; int nesting; bool fatal_error; bool creating_table; struct table *current_table; struct field *current_field; char *characters_buf; size_t characters_size; size_t characters_count; /* in PASS_1 used without _buf and _size */ char *pk_buf; size_t pk_size; size_t pk_filled; char *stmt_buf; size_t stmt_size; size_t stmt_filled; hashtable_t field_names; hashtable_t tables; MYSQL *mysql; FILE *raport_stream; }; /* * I'm not sure how I should interpret the 'bigger' and 'smaller' when it comes * to chars, that may or may not be signed. Anyways, no need to be coherent with * the original version because in practice this function is only ever used * to check equality. */ static int strcmp_replacing_dot(const char *s1, const char *s2) { size_t i = 0; int c1, c2; while (true) { c1 = (int) (unsigned char) s1[i]; c2 = (int) (unsigned char) s2[i]; if (c1 == '.') c1 = '_'; if (c2 == '.') c2 = '_'; if (c1 != c2) return c1 - c2; if (!c1) return 0; i++; } } static size_t string_hash_replacing_dot(const void *key_) { size_t i = 0, hash = (size_t) 0xa1bad2dead3beef4; char c, shift; const char *key = key_; do { c = key[i++]; if (c == '.') c = '_'; shift = ((unsigned char) c) % sizeof(size_t); hash += ((hash >> shift) | (hash << (sizeof(size_t) - shift))) ^ c; } while (c); return hash; } static void replace_dot(char *s) { do { if (*s == '.') *s = '_'; } while (*(s++)); } static int init_restore_data(struct restore *data, const char *raport_path) { data->errors_count = 0; data->pass = PASS_1; data->nesting = 0; data->fatal_error = false; data->creating_table = false; data->current_table = NULL; data->current_field = NULL; data->characters_buf = NULL; data->characters_size = 0; data->characters_count = 0; data->stmt_buf = NULL; data->stmt_size = 0; data->stmt_filled = 0; data->pk_buf = NULL; data->pk_size = 0; data->pk_filled = 0; if (make_field_names_ht(&data->field_names)) return -1; if (ht_init(&data->tables, string_hash_replacing_dot, (int (*)(const void*, const void*)) strcmp_replacing_dot)) { ht_destroy(&data->field_names); return -1; } data->raport_stream = raport_path ? fopen(raport_path, "w+") : NULL; if (!data->raport_stream) { if (raport_path) { fprintf(stderr, "Nie udalo sie utworzyc pliku raportu\n\n"); } data->raport_stream = stdout; } data->mysql = NULL; return 0; } static void free_field(struct field *field) { if (!field) return; if (is_rel_type(field->type)) free(field->spec_data.target_table); free(field); } static void free_table(struct table *table) { struct field *field, *tmp; if (!table) return; field = table->fields; while (field) { tmp = field->next; free_field(field); field = tmp; } free(table); } static void free_table_entry(void *key, void *val, void *dummy_arg) { free_table(val); } static void destroy_restore_data(struct restore *data) { ht_destroy(&data->field_names); ht_map_destroy(&data->tables, NULL, free_table_entry); free(data->characters_buf); free(data->stmt_buf); free(data->pk_buf); if (data->raport_stream != stderr) fclose(data->raport_stream); } static void out_of_memory_error(struct restore *data) { /* * TODO: dodac informacje na jakim etapie dzialania stanelo * i wypisać jakies informacje o dotychczas zrobionych rzeczach. */ data->fatal_error = true; fprintf(data->raport_stream, "Brak pamieci.\n"); } static void semantic_error(struct restore *data, const char *msg, ...) { va_list ap; data->fatal_error = true; fprintf(data->raport_stream, "Blad semantyczny xml: "); va_start(ap, msg); vfprintf(data->raport_stream, msg, ap); va_end(ap); fputc('\n', data->raport_stream); } struct sb_mysql_arg { MYSQL *mysql; const char *string; }; static int sb_mysql_escape(char **buf, size_t *buf_len, size_t *buf_filled, void *arg_) { struct sb_mysql_arg *arg = arg_; size_t len = strlen(arg->string); if (extend_buf(buf, buf_len, buf_filled, len * 2)) return -1; *buf_filled += mysql_real_escape_string(arg->mysql, *buf + *buf_filled, arg->string, len); return 0; } static int sb_name_escape(char **buf, size_t *buf_len, size_t *buf_filled, void *name_) { const unsigned char *name = name_; size_t i; for (i = 0; name[i]; i++) { if (name[i] == '`') { if (sb_bytes(buf, buf_len, buf_filled, name, i + 1)) return -1; name += i; i = 0; } } return sb_bytes(buf, buf_len, buf_filled, name, i); } static bool is_field_compatible(struct restore *data, const char *name, enum field_type type, const char *target_table) { struct field *previous = data->current_field; return !strcmp(previous->name, (const char*) name) && previous->type == type && (!is_rel_type(type) || !strcmp_replacing_dot(target_table, previous->spec_data.target_table)); } static void new_field(struct restore *data, const char *name, enum field_type type, const char *target_table) { struct field *field = NULL, **dst; size_t table_name_size; field = create_field(name); if (!field) goto fail; field->next = NULL; field->type = type; if (is_rel_type(type)) { table_name_size = strlen(target_table) + 1; field->spec_data.target_table = malloc(table_name_size); if (!field->spec_data.target_table) goto fail; memcpy(field->spec_data.target_table, target_table, table_name_size); replace_dot(field->spec_data.target_table); } else if (type == ARRAY_FIELD) { field->spec_data.array_has_data = false; } else { field->spec_data.max_length = 0; } dst = data->current_field ? &data->current_field->next : &data->current_table->fields; *dst = field; data->current_field = field; return; fail: free_field(field); out_of_memory_error(data); } static void new_table(struct restore *data, const char *table_name) { struct table *table = NULL; table = create_table(table_name); if (!table) goto fail; replace_dot(table->name); table->creation_error = false; table->all_rows = 0; table->inserted_rows = 0; table->insertion_errors = 0; table->fields = NULL; if (ht_add(&data->tables, table->name, table)) goto fail; data->creating_table = true; data->current_table = table; data->current_field = NULL; return; fail: free_table(table); out_of_memory_error(data); } static int find_attrs(const char **attrs, int to_find, const char *const *searched, const char **found) { int i, j; /* `i' iterates through attrs[], `j' through searched */ int found_count = 0; if (!attrs) return to_find; for (j = 0; j < to_find; j++) found[j] = NULL; for (i = 0; attrs[i]; i += 2) { for (j = 0; j < to_find; j++) { if (!found[j] && !strcmp(attrs[i], searched[j])) { found_count++; found[j] = attrs[i + 1]; if (to_find == found_count) return 0; } } } return to_find - found_count; } /* * TODO: change handle_field(), handle_entry() and other functions below and * above not to call exit(), but rather let control return to main(). */ static void handle_field(struct restore *data, const char *name, const char **attrs) { const char *searched[4] = {"name", "type", "rel", "to"}; const char *found[4]; const char *type_name; const enum field_type *type; data->characters_count = 0; if (data->pass == PASS_2) return; if (strcmp(name, "field")) { semantic_error(data, "tag '%s' zamiast 'field' na poziomie 2", name); return; } find_attrs(attrs, 4, searched, found); if (!found[0]) { semantic_error(data, "brak atrybutu 'name' w polu modelu `%s`", data->current_table->name); return; } type_name = found[2] ? found[2] : found[1] ? found[1] : NULL; if (!type_name) { semantic_error(data, "brak atrybutu 'type' w polu `%s` modelu `%s`", found[0], data->current_table->name); return; } if (ht_get(&data->field_names, type_name, NULL, (void**) &type)) { fprintf(data->raport_stream, "nieznany typ '%s' kolumny `%s` tabeli `%s`.\n", type_name, found[0], data->current_table->name); type = &field_type_ids[UNKNOWN_FIELD]; } if (is_rel_type(*type) && !found[3]) { semantic_error(data, "brak atrybutu 'to' w polu `%s` modelu `%s`", found[0], data->current_table->name); return; } if (data->creating_table) { new_field(data, found[0], *type, found[3]); } else if (!data->current_field) { semantic_error(data, "zmienna liczba pol w rekordach modelu `%s`", data->current_table->name); return; } else if (!is_field_compatible(data, found[0], *type, found[3])){ semantic_error(data, "niekompatybilne rekordy modelu `%s`", data->current_table->name); return; } return; } static void handle_entry(struct restore *data, const char *name, const char **attrs) { const char *searched[2] = {"model", "pk"}; const char *found[2]; struct table *table; struct sb_mysql_arg sb_arg; int ret; if (strcmp(name, "object")) { semantic_error(data, "tag '%s' zamiast 'object' na poziomie 1", name); return; } find_attrs(attrs, 2, searched, found); if (!found[0]) { semantic_error(data, "brak atrybutu 'model' w obiekcie"); return; } if (!found[1]) { semantic_error(data, "brak atrybutu 'pk' w obiekcie"); return; } ret = ht_get(&data->tables, found[0], NULL, (void**) &table); if (ret == HT_KEY_ABSENT) { switch (data->pass) { case PASS_1: new_table(data, found[0]); break; case PASS_2: goto fail; } } else { data->creating_table = false; if (data->pass == PASS_2 && data->current_table != table) printf("Wypelnianie tabeli `%s`\n", table->name); data->current_table = table; /* not really needed in PASS_1 */ data->current_field = table->fields; } if (data->pass != PASS_2) return; data->pk_filled = 0; if (sb_string(&data->pk_buf, &data->pk_size, &data->pk_filled, found[1])) goto fail; data->stmt_filled = 0; sb_arg = (struct sb_mysql_arg) {data->mysql, found[1]}; if (sb_sprintf(&data->stmt_buf, &data->stmt_size, &data->stmt_filled, "INSERT INTO `%_` VALUES(\n" "'%_'", sb_name_escape, table->name, sb_mysql_escape, &sb_arg)) goto fail; return; fail: data->fatal_error = true; } static int insert_to_connector_table(struct restore *data, const char **attrs) { int res = -1; struct field *field = data->current_field; const char *tab1 = data->current_table->name; const char *tab2 = field->spec_data.target_table; const char *pk_attr = "pk"; const char *pk; char *stmt_buf = NULL; size_t stmt_size = 0, stmt_filled = 0; struct sb_mysql_arg sb_arg1, sb_arg2; if (!tab2) /* Means error during creation of conenctor table */ return 0; find_attrs(attrs, 1, &pk_attr, &pk); if (!pk) { semantic_error(data, "Brak atrybutu 'pk' w tagu w relacji wiele-do-wielu.\n"); goto fail; } sb_arg1 = (struct sb_mysql_arg) {data->mysql, data->pk_buf}; sb_arg2 = (struct sb_mysql_arg) {data->mysql, pk}; if (sb_sprintf(&stmt_buf, &stmt_size, &stmt_filled, "INSERT INTO `%__mtm_%_` VALUES(\n" "'%_',\n" "'%_');", sb_name_escape, tab1, sb_name_escape, tab2, sb_mysql_escape, &sb_arg1, sb_mysql_escape, &sb_arg2)) goto fail; if (mysql_real_query(data->mysql, stmt_buf, stmt_filled)) { data->errors_count++; if (!data->current_table->insertion_errors++) { fprintf(data->raport_stream, "Nie udalo sie wstawic elementu do tabeli lacznikowej:\n" "Blad %u (%s): %s\n", mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql)); } } res = 0; fail: free(stmt_buf); return res; } static void startElement_callback(void *user_data, const xmlChar *name_, const xmlChar **attrs_) { const char *name = (const char*) name_; const char **attrs = (const char**) attrs_; struct restore *data = user_data; if (data->fatal_error) return; switch(data->nesting++) { case 0: /* We have no interest in the root element */ return; case 1: handle_entry(data, name, attrs); break; case 2: handle_field(data, name, attrs); break; case 3: if (data->pass == PASS_2 && (data->current_field->type == MANY_TO_MANY_REL || data->current_field->type == BROKEN_TO_MANY_REL) && insert_to_connector_table(data, attrs)) goto fail; default: break; } return; fail: data->fatal_error = true; } static int insert_CHAR_FIELD(struct restore *data) { struct sb_mysql_arg sb_arg; if (!data->characters_count && sb_string(&data->stmt_buf, &data->stmt_size, &data->stmt_filled, ",\nNULL")) return -1; sb_arg = (struct sb_mysql_arg) {data->mysql, data->characters_buf}; if (data->characters_count && sb_sprintf(&data->stmt_buf, &data->stmt_size, &data->stmt_filled, ",\n'%_'", sb_mysql_escape, &sb_arg)) return -1; return 0; } #define insert_FILE_FIELD insert_CHAR_FIELD #define insert_TEXT_FIELD insert_CHAR_FIELD #define insert_FILE_FIELD insert_CHAR_FIELD #define insert_INT_FIELD insert_CHAR_FIELD #define insert_SMALL_INT_FIELD insert_CHAR_FIELD #define insert_POSITIVE_INT_FIELD insert_CHAR_FIELD #define insert_BIG_INT_FIELD insert_CHAR_FIELD #define insert_DECIMAL_FIELD insert_CHAR_FIELD #define insert_TIME_FIELD insert_CHAR_FIELD #define insert_DATETIME_FIELD insert_CHAR_FIELD #define insert_DATE_FIELD insert_CHAR_FIELD #define insert_JSON_FIELD insert_CHAR_FIELD #define insert_ONE_TO_ONE_REL insert_CHAR_FIELD #define insert_MANY_TO_ONE_REL insert_CHAR_FIELD #define insert_BROKEN_TO_ONE_REL insert_CHAR_FIELD #define insert_UNKNOWN_FIELD insert_CHAR_FIELD static int insert_BOOL_FIELD(struct restore *data) { const char *value; if (data->characters_count && strstr(data->characters_buf, "False")) value = ",\nFALSE"; else if (data->characters_count && strstr(data->characters_buf, "True")) value = ",\nTRUE"; else value = ",\nNULL"; if (sb_string(&data->stmt_buf, &data->stmt_size, &data->stmt_filled, value)) return -1; return 0; } static int insert_array_element(struct restore *data, const char *element, int *current_id) { int res = -1; char *stmt_buf = NULL; size_t stmt_size = 0, stmt_filled = 0; struct sb_mysql_arg sb_arg1, sb_arg2; sb_arg1 = (struct sb_mysql_arg) {data->mysql, data->pk_buf}; sb_arg2 = (struct sb_mysql_arg) {data->mysql, element}; if (sb_sprintf(&stmt_buf, &stmt_size, &stmt_filled, "INSERT INTO `%__array_%_` VALUES(\n" "%d,\n" "'%_',\n" "'%_');", sb_name_escape, data->current_table->name, sb_name_escape, data->current_field->name, (*current_id)++, sb_mysql_escape, &sb_arg1, sb_mysql_escape, &sb_arg2)) goto fail; if (mysql_real_query(data->mysql, stmt_buf, stmt_filled)) { data->errors_count++; if (!data->current_table->insertion_errors++) { fprintf(data->raport_stream, "Nie udalo sie wstawic elementu danej ARRAY:\n" "Blad %u (%s): %s\n", mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql)); } } res = 0; fail: free(stmt_buf); return res; } static int insert_ARRAY_FIELD(struct restore *data) { char *pos, *string_start; unsigned char c; int current_id = 0; enum { NOT_STARTED, BEFORE_FIRST_STRING, BEFORE_STRING, IN_STRING, AFTER_STRING, FINISHED } state = NOT_STARTED; for (pos = data->characters_buf; *pos; pos++) { c = *pos; switch (state) { case NOT_STARTED: if (isspace(c)) break; if (c == '[') { state = BEFORE_FIRST_STRING; break; } goto syntax_fail; case BEFORE_FIRST_STRING: case BEFORE_STRING: if (isspace(c)) break; if (c == '"') { state = IN_STRING; string_start = pos + 1; break; } if (c == ']' && state == BEFORE_FIRST_STRING) { state = FINISHED; break; } goto syntax_fail; case IN_STRING: if (c != '"' || pos[-1] == '\\') break; *pos = '\0'; if (insert_array_element(data, string_start, ¤t_id)) goto fail; state = AFTER_STRING; break; case AFTER_STRING: if (isspace(c)) break; if (c == ',') { state = BEFORE_STRING; break; } if (c == ']') { state = FINISHED; break; } goto syntax_fail; case FINISHED: if (!isspace(c)) goto syntax_fail; } } if (state != FINISHED) goto fail; return 0; syntax_fail: fprintf(data->raport_stream, "Blad syntaktyczny w danej typu array:\n%s\n", data->characters_buf); fail: return -1; } static int no_insert_field(struct restore *data) { return 0; } /* Deliberately omitted */ #define insert_EMPTY_ARRAY_FIELD no_insert_field /* Inserts to connector tables happen elsewhere */ #define insert_MANY_TO_MANY_REL no_insert_field #define insert_BROKEN_TO_MANY_REL no_insert_field #define X(type, name) [type] = insert_##type, #define Y(type, name) X(type, name) static int (*field_inserters[])(struct restore *) = { #include "field_types.c" }; #undef X #undef Y static int finalize_insert(struct restore *data) { if (sb_string(&data->stmt_buf, &data->stmt_size, &data->stmt_filled, ");")) return -1; data->current_table->all_rows++; if (mysql_real_query(data->mysql, data->stmt_buf, data->stmt_filled)) { data->errors_count++; if (!data->current_table->insertion_errors++) { fprintf(data->raport_stream, "Nie udalo sie wstawic rekordu do bazy:\n" "Blad %u (%s): %s\n" "Zapytanie:\n" "%s\n", mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql), data->stmt_buf); } } else { data->current_table->inserted_rows++; } return 0; } static void endElement_callback(void *user_data, const xmlChar *name) { struct restore *data = user_data; struct field *field = data->current_field; if (data->fatal_error) return; switch (data->nesting--) { case 3: break; case 2: if (data->pass == PASS_2 && !data->current_table->creation_error && finalize_insert(data)) goto fail; default: return; } switch (data->pass) { case PASS_1: if (!field) goto out; if (is_text_type(field->type) && field->spec_data.max_length < data->characters_count) field->spec_data.max_length = data->characters_count; if (field->type == ARRAY_FIELD && strchr(data->characters_buf, '"')) field->spec_data.array_has_data = true; break; case PASS_2: if (!field) goto fail; if (field_inserters[field->type](data)) goto fail; } if (!data->creating_table) data->current_field = field->next; out: return; fail: data->fatal_error = true; } static void characters_callback(void *user_data, const xmlChar *characters, int len) { struct restore *data = user_data; if (data->fatal_error) return; /* Should not happen, but... */ if (!data->current_field) { if (data->pass == PASS_1) fprintf(data->raport_stream, "Znaki poza tagiem 'field': '%.*s'\n", len, characters); return; } switch (data->pass) { case PASS_1: if (data->current_field->type != ARRAY_FIELD) { data->characters_count += len; break; } case PASS_2: if (sb_bytes(&data->characters_buf, &data->characters_size, &data->characters_count, characters, len)) data->fatal_error = true; } } static void error_callback(void *user_data, const char *msg, ...) { struct restore *data = user_data; va_list ap; int c; if (data->fatal_error) return; fprintf(data->raport_stream, "Blad xml: "); va_start(ap, msg); vfprintf(data->raport_stream, msg, ap); va_end(ap); if (!data->errors_count++) { fprintf(stderr, "Blad syntaktyczny xml, kontynuowac? y/N: "); fflush(stderr); c = getchar(); if (c != 'y' && c != 'Y') data->fatal_error = true; } } static int clear_database(struct restore *data, const char *dbname) { int res = -1; char *stmt_buf = NULL; size_t buf_len = 0, buf_filled = 0; struct sb_mysql_arg sb_arg; MYSQL_RES *query_res = NULL; size_t i; my_ulonglong rows; MYSQL_ROW *rows_array = NULL; sb_arg = (struct sb_mysql_arg) {data->mysql, dbname}; if (sb_sprintf(&stmt_buf, &buf_len, &buf_filled, "SELECT table_name " "FROM information_schema.tables " "WHERE table_schema = '%_';", sb_mysql_escape, &sb_arg)) goto fail; if (mysql_real_query(data->mysql, stmt_buf, buf_filled)) { fprintf(data->raport_stream, "Nie udalo sie pobrac nazw istniejacych tabel:\n" "Blad %u (%s): %s\n" "Zapytanie:\n" "%s\n", mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql), stmt_buf); goto fail; } query_res = mysql_store_result(data->mysql); if (!query_res) goto fail; rows = mysql_num_rows(query_res); rows_array = malloc(sizeof(MYSQL_ROW) * rows); if (!rows_array) goto fail; for (i = 0; i < rows; i++) { rows_array[i] = mysql_fetch_row(query_res); if (!rows_array[i]) goto fail; } for (i = 0; i < rows; i++) { printf("Usuwanie tabeli `%s`.\n", rows_array[i][0]); buf_filled = 0; /* Assume fetched table names are not malicious */ if (sb_sprintf(&stmt_buf, &buf_len, &buf_filled, "DROP TABLE IF EXISTS `%_`;", sb_name_escape, rows_array[i][0])) goto fail; if (mysql_real_query(data->mysql, stmt_buf, buf_filled)) { fprintf(data->raport_stream, "Nie udalo sie usunac instniejacej tabeli:\n" "Blad %u (%s): %s\n", mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql)); goto fail; } } putchar('\n'); res = 0; fail: free(rows_array); if (query_res) mysql_free_result(query_res); free(stmt_buf); return res; } static int sb_field_name(char **stmt_buf, size_t *buf_len, size_t *buf_filled, struct field *field) { if (sb_sprintf(stmt_buf, buf_len, buf_filled, ",\n" "`%_` ", sb_name_escape, field->name)) return -1; return 0; } #define SIMPLE_FIELD_DECLARATOR(type, sql_name) \ static int sb_##type(char **stmt_buf, size_t *buf_len, \ size_t *buf_filled, struct field *field) \ { \ if (sb_field_name(stmt_buf, buf_len, \ buf_filled, field) || \ sb_string(stmt_buf, buf_len, buf_filled, sql_name)) \ return -1; \ \ return 0; \ } SIMPLE_FIELD_DECLARATOR(TEXT_FIELD, "TEXT") SIMPLE_FIELD_DECLARATOR(INT_FIELD, "INT") SIMPLE_FIELD_DECLARATOR(SMALL_INT_FIELD, "SMALLINT") SIMPLE_FIELD_DECLARATOR(POSITIVE_INT_FIELD, "INT UNSIGNED") SIMPLE_FIELD_DECLARATOR(BIG_INT_FIELD, "BIGINT") SIMPLE_FIELD_DECLARATOR(DECIMAL_FIELD, "DECIMAL") SIMPLE_FIELD_DECLARATOR(BOOL_FIELD, "BOOL") SIMPLE_FIELD_DECLARATOR(TIME_FIELD, "TIME") SIMPLE_FIELD_DECLARATOR(DATETIME_FIELD, "DATETIME") SIMPLE_FIELD_DECLARATOR(DATE_FIELD, "DATE") SIMPLE_FIELD_DECLARATOR(JSON_FIELD, "TEXT") /* We do foreign key constraints later, now we just create an int column */ SIMPLE_FIELD_DECLARATOR(ONE_TO_ONE_REL, "INT") SIMPLE_FIELD_DECLARATOR(MANY_TO_ONE_REL, "INT") SIMPLE_FIELD_DECLARATOR(BROKEN_TO_ONE_REL, "INT") SIMPLE_FIELD_DECLARATOR(UNKNOWN_FIELD, "TEXT") static int sb_CHAR_FIELD(char **stmt_buf, size_t *buf_len, size_t *buf_filled, struct field *field) { if (sb_field_name(stmt_buf, buf_len, buf_filled, field) || sb_sprintf(stmt_buf, buf_len, buf_filled, "VARCHAR(%u)", (unsigned) field->spec_data.max_length)) return -1; return 0; } #define sb_FILE_FIELD sb_CHAR_FIELD static int no_sb_field(char **stmt_buf, size_t *buf_len, size_t *buf_filled, struct field *field) { return 0; } /* Deliberately omitted */ #define sb_EMPTY_ARRAY_FIELD no_sb_field /* Realized in terms of separate tables */ #define sb_ARRAY_FIELD no_sb_field #define sb_MANY_TO_MANY_REL no_sb_field #define sb_BROKEN_TO_MANY_REL no_sb_field #define X(type, name) [type] = sb_##type, #define Y(type, name) X(type, name) static int (*field_declarators[])(char**, size_t*, size_t*, struct field*) = { #include "field_types.c" }; #undef X #undef Y static void create_mysql_table(const void *key, void *val, void *data_) { bool fatal_error = true; struct table *table = val; struct field *field; struct restore *data = data_; char *stmt_buf = NULL; size_t buf_len = 0, buf_filled = 0; if (data->fatal_error) return; printf("Tworzenie tabeli `%s`.\n", table->name); if (sb_sprintf(&stmt_buf, &buf_len, &buf_filled, "CREATE TABLE `%_` (\n" "ID INT PRIMARY KEY", sb_name_escape, table->name)) goto fail; for (field = table->fields; field; field = field->next) { if (field_declarators[field->type](&stmt_buf, &buf_len, &buf_filled, field)) goto fail; } if (sb_string(&stmt_buf, &buf_len, &buf_filled, ");")) goto fail; if (mysql_real_query(data->mysql, stmt_buf, buf_filled)) { data->errors_count++; fprintf(data->raport_stream, "Nie udalo sie utworzyc tabeli `%s`:\n" "Blad %u (%s): %s\n" "Zapytanie:\n" "%s\n", table->name, mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql), stmt_buf); table->creation_error = true; } fatal_error = false; fail: free(stmt_buf); if (fatal_error) data->fatal_error = true; } static int create_mysql_tables(struct restore *data) { ht_map(&data->tables, data, create_mysql_table); if (data->fatal_error) return -1; putchar('\n'); return 0; } static void check_table_relations_correctness(const void *key, void *val, void *data_) { struct restore *data = data_; struct table *table = val, *target_table; struct field *field; for (field = table->fields; field; field = field->next) { if (field->type == ARRAY_FIELD && !field->spec_data.array_has_data) { fprintf(data->raport_stream, "Pole `%s` typu ARRAY tabeli `%s` nie zawiera danych, bedzie ono pominiete w odtworzonej bazie.\n", field->name, table->name); field->type = EMPTY_ARRAY_FIELD; } if (!is_rel_type(field->type)) continue; target_table = NULL; /* * I wrote that hashtable. I can assume certain ht api functions * are re-entrant... Can I? */ if (!ht_get(&data->tables, field->spec_data.target_table, NULL, (void**) &target_table) && !target_table->creation_error) continue; fprintf(data->raport_stream, "Relacja %s z tabeli `%s` (pole `%s`) do nie%s tabeli `%s`\n", field_enum_names[field->type], table->name, field->name, target_table ? "dodanej" : "istniejacej", field->spec_data.target_table); field->type = field->type == MANY_TO_MANY_REL ? BROKEN_TO_MANY_REL : BROKEN_TO_ONE_REL; } } static void check_tables_relations(struct restore *data) { /* It's a bit of a cheat - assuming I can call ht_get() while mapping */ ht_map(&data->tables, data, check_table_relations_correctness); fputc('\n', data->raport_stream); } static void fprint_field(FILE *os, const struct field *field) { fprintf(os, " `%s` %s", field->name, field_enum_names[field->type]); if (is_rel_type(field->type)) fprintf(os, " to `%s`", field->spec_data.target_table); if (is_text_type(field->type)) { fprintf(os, " max %lu", (unsigned long) field->spec_data.max_length); } fputc('\n', os); } static void fprint_table(FILE *os, const struct table *table) { struct field *field; fprintf(os, "Tabela `%s`\n", table->name); for (field = table->fields; field; field = field->next) fprint_field(os, field); putc('\n', os); } static void fprint_table_entry(const void *key, void *val, void *os) { fprint_table(os, val); } static void fprint_all_tables(FILE *os, hashtable_t *tables) { ht_map(tables, os, fprint_table_entry); } static int add_fk_constraint(struct restore *data, struct table *table, struct field *field) { int res = -1; char *stmt_buf = NULL; size_t buf_len = 0, buf_filled = 0; printf("Nakladanie ograniczenia na klucz obcy `%s` w tabeli `%s`.\n", field->name, table->name); if (sb_sprintf(&stmt_buf, &buf_len, &buf_filled, "ALTER TABLE `%_`\n" "ADD CONSTRAINT `%__%__FK` FOREIGN KEY (`%_`)\n" "REFERENCES `%_`(id);", sb_name_escape, table->name, sb_name_escape, table->name, sb_name_escape, field->name, sb_name_escape, field->name, sb_name_escape, field->spec_data.target_table)) goto out; if (mysql_real_query(data->mysql, stmt_buf, buf_filled)) { data->errors_count++; fprintf(data->raport_stream, "Nie udalo sie dodac ograniczenia klucza obcego na kolumne `%s` tabeli `%s`:\n" "Blad %u (%s): %s\n", field->name, table->name, mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql)); } res = 0; out: free(stmt_buf); return res; } static int sb_maybe_references(char **buf, size_t *buf_len, size_t *buf_filled, void *table_name) { if (!table_name) return 0; return sb_sprintf(buf, buf_len, buf_filled, " REFERENCES `%_`(id)", sb_name_escape, table_name); } static int create_mysql_mtm_table(struct restore *data, struct table *table, struct field *field) { int res = -1; char *stmt_buf = NULL; size_t buf_len = 0, buf_filled = 0; char *tab1 = table->name, *tab2 = field->spec_data.target_table; printf("Tworzenie tabeli lacznikowej `%s_mtm_%s`.\n", tab1, tab2); if (sb_sprintf(&stmt_buf, &buf_len, &buf_filled, "CREATE TABLE `%__mtm_%_` (\n" "`%__id` INT%_,\n" "`%__%__id` INT%_,\n" "PRIMARY KEY(`%__id`, `%__%__id`));", sb_name_escape, tab1, sb_name_escape, tab2, sb_name_escape, tab1, sb_maybe_references, table->creation_error ? NULL : tab1, sb_name_escape, field->name, sb_name_escape, tab2, sb_maybe_references, field->type == BROKEN_TO_MANY_REL ? NULL : tab2, sb_name_escape, tab1, sb_name_escape, field->name, sb_name_escape, tab2)) goto fail; if (mysql_real_query(data->mysql, stmt_buf, buf_filled)) { data->errors_count++; fprintf(data->raport_stream, "Nie udalo sie utworzyc tabeli lacznikowej `%s_mtm_%s`:\n" "Blad %u (%s): %s\n" "Zapytanie:\n" "%s\n", tab1, tab2, mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql), stmt_buf); field->spec_data.target_table = NULL; } res = 0; fail: free(stmt_buf); return res; } static int create_mysql_array(struct restore *data, struct table *table, struct field *field) { int res = -1; char *stmt_buf = NULL; size_t buf_len = 0, buf_filled = 0; printf("Tworzenie tabeli `%s_array_%s` do zrealizowania pola ARRAY.\n", table->name, field->name); if (sb_sprintf(&stmt_buf, &buf_len, &buf_filled, "CREATE TABLE `%__array_%_` (\n" "id_of_element INT,\n" "`%__id` INT%_,\n" "element VARCHAR(255),\n" "PRIMARY KEY(`%__id`, id_of_element));", sb_name_escape, table->name, sb_name_escape, field->name, sb_name_escape, table->name, sb_maybe_references, table->creation_error ? NULL : table->name, sb_name_escape, table->name)) goto fail; if (mysql_real_query(data->mysql, stmt_buf, buf_filled)) { data->errors_count++; fprintf(data->raport_stream, "Nie udalo sie utworzyc tabeli `%s_array%s` realizujacej pole typu ARRAY:\n" "Blad %u (%s): %s\n" "Zapytanie:\n" "%s\n", table->name, field->name, mysql_errno(data->mysql), mysql_sqlstate(data->mysql), mysql_error(data->mysql), stmt_buf); field->type = EMPTY_ARRAY_FIELD; } res = 0; fail: free(stmt_buf); return res; } static void create_mysql_table_relations(const void *key, void *val, void *data_) { struct table *table = val; struct field *field; struct restore *data = data_; if (data->fatal_error) return; for (field = table->fields; field; field = field->next) { if ((field->type == MANY_TO_MANY_REL || field->type == BROKEN_TO_MANY_REL) && create_mysql_mtm_table(data, table, field)) goto fail; if (field->type == ARRAY_FIELD && create_mysql_array(data, table, field)) goto fail; if (table->creation_error) continue; if ((field->type == ONE_TO_ONE_REL || field->type == MANY_TO_ONE_REL) && add_fk_constraint(data, table, field)) goto fail; } return; fail: data->fatal_error = true; } static int complete_mysql_tables(struct restore *data) { ht_map(&data->tables, data, create_mysql_table_relations); if (data->fatal_error) return -1; putchar('\n'); return 0; } static void print_table_summary(const void *key, void *val, void *data_) { struct table *table = val; struct restore *data = data_; if (!table->all_rows) { fprintf(data->raport_stream, "Nie wstawiono zadnych rekordow do tabeli `table->name`.\n"); return; } fprintf(data->raport_stream, "Rekordy wstawione do tabeli `%s`: %ld/%ld\n", table->name, (long) table->inserted_rows, (long) table->all_rows); if (!table->insertion_errors) return; fprintf(data->raport_stream, "Bledy przy wypelnianiu tabeli `%s`: %ld\n", table->name, (long) table->insertion_errors); } static void print_entire_summary(struct restore *data) { ht_map(&data->tables, data, print_table_summary); fputc('\n', data->raport_stream); } static inline void error_exit(const char *msg, ...) { va_list ap; va_start(ap, msg); vfprintf(stderr, msg, ap); va_end(ap); fputc('\n', stderr); exit(-1); } const char foreign_checks_off[] = "SET FOREIGN_KEY_CHECKS = 0;"; const char foreign_checks_on[] = "SET FOREIGN_KEY_CHECKS = 1;"; const char set_utf8[] = "SET NAMES 'utf8';"; int main(int argc, char **argv) { const char *xml_path, *db_host, *db_user, *db_password, *db_name, *raport_path = NULL; int db_port; xmlSAXHandler sax1, sax2; struct restore data; int i; if (argc < 7) { error_exit("Zbyt malo argumentów. Oczekiwane:\n" "restore SCIEZKA_PLIKU_XML ADRES_BAZY PORT UZYTKOWNIK HASLO NAZWA_BAZY [SCIEZKA_PLIKU_RAPORTU]"); } xml_path = argv[1]; db_host = argv[2]; db_port = atoi(argv[3]); db_user = argv[4]; db_password = argv[5]; db_name = argv[6]; if (argc >= 8) raport_path = argv[7]; memset(&sax1, 0, sizeof(sax1)); if (init_restore_data(&data, raport_path)) error_exit("Blad inicjalizacji wewnetrznych struktur."); sax1.initialized = XML_SAX2_MAGIC; sax1.startElement = startElement_callback; sax1.endElement = endElement_callback; sax1.characters = characters_callback; sax2 = sax1; sax1.error = error_callback; data.pass = PASS_1; xmlSAXUserParseFile(&sax1, &data, xml_path); if (data.fatal_error || !data.tables.entries) error_exit("Nie udalo sie ustalic schematu bazy."); if (data.errors_count) { fprintf(data.raport_stream, "Byly bledy podczas czytania pliku xml (%ld).\n", (long) data.errors_count); } putc('\n', data.raport_stream); /* initialize connection handler */ data.mysql = mysql_init(NULL); if (!data.mysql) error_exit("Blad inicializacji libmysql (prawdopodobnie brak pamieci)."); /* connect to server */ if (!mysql_real_connect(data.mysql, db_host, db_user, db_password, db_name, db_port, NULL, 0)) { fprintf(data.raport_stream, "Nie udalo sie polaczyc z baza.\n" "Blad %u (%s): %s\n", mysql_errno(data.mysql), mysql_sqlstate(data.mysql), mysql_error(data.mysql)); mysql_close(data.mysql); return -1; } printf("Polaczono z baza\n\n"); /* Re-enabling foreign checks later is not needed. */ if (mysql_real_query(data.mysql, foreign_checks_off, sizeof(foreign_checks_off) - 1)) { fprintf(data.raport_stream, "Nie udalo sie wylaczyc sprawdzania spojnosci relacji.\n" "Blad %u (%s): %s\n", mysql_errno(data.mysql), mysql_sqlstate(data.mysql), mysql_error(data.mysql)); error_exit("Blad podczas wylaczania sprawdzania spojnosci relacji."); } /* Setting utf-8 for communication between database and the client. */ if (mysql_real_query(data.mysql, set_utf8, sizeof(set_utf8) - 1)) { data.errors_count++; fprintf(data.raport_stream, "Nie udalo sie ustawic kodowania utf8.\n" "Blad %u (%s): %s\n", mysql_errno(data.mysql), mysql_sqlstate(data.mysql), mysql_error(data.mysql)); } if (clear_database(&data, "nowabaze")) { error_exit("Blad podczas usuwania starej zawartosci bazy."); } if (create_mysql_tables(&data)) error_exit("Blad podczas tworzenia tabel."); check_tables_relations(&data); fprint_all_tables(data.raport_stream, &data.tables); if (complete_mysql_tables(&data)) error_exit("Blad podczas uzupelniania utworzonych tabel."); data.pass = 2; xmlSAXUserParseFile(&sax2, &data, xml_path); putchar('\n'); fputc('\n', data.raport_stream); print_entire_summary(&data); if (data.fatal_error) error_exit("Fatalny blad podczas umieszczania danych w bazie."); #define OS(n) ((n) == 0 ? data.raport_stream : stdout) for (i = data.raport_stream == stdout ? 0 : 1; i >= 0; i--) { fprintf(OS(i), "Operacja zakonczona"); if (data.errors_count) { fprintf(OS(i), "; bledow lacznie: %ld\n", (long) data.errors_count); } else { fprintf(OS(i), " bez bledow.\n"); } } mysql_close(data.mysql); destroy_restore_data(&data); return 0; }