/* * GtkSFV.c-v0.07: * GtkSFV is a simple GTK2-based SFV and MD5 verification app, with a UI * similar to QuickSFV (http://www.geocities.com/SiliconValley/Mouse/4668/). * * Requirements: * GTK2 - http://www.gtk.org/ --- tested with 2.2.2 * * Compiling: * gcc -Wall -g GtkSFV.c -o GtkSFV \ * `pkg-config --cflags gtk+-2.0` `pkg-config --libs gtk+-2.0` * * Usage: * GtkSFV * * Notes: * Processing large files will hang GtkSFV. Only thing I can think of to * fix that is to check the files in a child process... * * (c) 2003 Mike Hokenson * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #define GTKSFV_VERSION "0.07" #define GTKSFV_AUTHOR "2003 Mike Hokenson " #define GTKSFV_URL "http://www.gozer.org/my_stuff/gtk/GtkSFV.php" #ifndef PATH_MAX # ifdef MAXPATHLEN # define PATH_MAX MAXPATHLEN # else # define PATH_MAX 1024 # endif #endif #ifndef LINE_MAX #define LINE_MAX 2048 #endif #define _PATH_RC ".gtksfvrc" #define IS_TRUE(a) ((g_ascii_strcasecmp(a, "TRUE") == 0)) #define IS_FALSE(a) ((g_ascii_strcasecmp(a, "FALSE") == 0)) #define IS_EXIST(a) ((g_file_test(a, G_FILE_TEST_EXISTS))) #define IS_FILE(a) ((g_file_test(a, G_FILE_TEST_IS_REGULAR))) #define IS_DIR(a) ((g_file_test(a, G_FILE_TEST_IS_DIR))) #define G_ERRMSG ((g_strerror(errno))) #define G_PERROR(a) ((g_print("%s: %s\n", a, G_ERRMSG))) #define STREQ(a, b) (((a && b) && strcmp(a, b) == 0)) #define STRNEQ(a, b, c) (((a && b) && strncmp(a, b, c) == 0)) #define _SFV_ST_MISSING "MISSING" #define _SFV_ST_OK "OK" #define _SFV_ST_BAD "BAD" #define _SFV_ST_ERROR "ERROR" #define _SFV_MD5_LEN 32 #define _SFV_SFV_LEN 8 /* used for GtkListStore */ enum { SFV_NAME, SFV_STATUS, SFV_STORED_CRC, SFV_LOCAL_CRC, SFV_N_COLUMNS }; /* crc types */ enum { SFV_SFV, SFV_MD5, SFV_NONE }; /* file dialog types */ enum { SFV_FILEDIALOG_OPEN, SFV_FILEDIALOG_NEW, SFV_FILEDIALOG_SAVE }; /* crc entry */ struct gtksfv_entry_t { gchar file[PATH_MAX]; gchar local_crc[_SFV_MD5_LEN + 1]; gchar stored_crc[_SFV_MD5_LEN + 1]; gchar *status; }; /* statusbars */ struct gtksfv_sb_t { gchar *desc; GtkWidget *widget; guint cid; }; void gtksfv_open(void); void gtksfv_new_sfv(void); void gtksfv_new_md5(void); void gtksfv_save(void); void gtksfv_quit(void); void gtksfv_exit(void); void gtksfv_about(void); void gtksfv_load_config(void); void gtksfv_save_config(void); void gtksfv_window_set_title(const gchar *file); void gtksfv_label_set_markup(GtkLabel *label, const gchar *text); GtkWidget *gtksfv_label_new(const gchar *fmt, ...); gint gtksfv_window_event_handler(GtkWidget *widget, GdkEvent *event, GtkWidget *window); GtkListStore *gtksfv_list_store_create(GtkWidget *widget); GtkWidget *gtksfv_create_menu(void); struct gtksfv_sb_t *gtksfv_create_statusbar(const gchar *desc); void gtksfv_reset_statusbars(void); void gtksfv_statusbar_push(struct gtksfv_sb_t *sb, const gchar *fmt, ...); gboolean gtksfv_save_sfv(const gchar *file); void gtksfv_write_sfv_header(FILE *fp); void gtksfv_write_sfv_comment(struct gtksfv_entry_t *s, FILE *fp); void gtksfv_write_sfv_line(struct gtksfv_entry_t *s, FILE *fp); gboolean gtksfv_close_sfv(void); gboolean gtksfv_process_sfv(const gchar *file); gboolean gtksfv_check_sfv_file(const gchar *file); void gtksfv_update_sfv_statusbars(guint ok, guint missing, guint bad, guint total); gchar *gtksfv_resolve_sfv_file(const gchar *file); struct gtksfv_entry_t *gtksfv_parse_sfv_line(const gchar *line); gchar *gtksfv_get_file_crc(const gchar *file); struct gtksfv_entry_t *gtksfv_entry_new(void); void gtksfv_list_store_clear(void); GSList *gtksfv_get_list_store(void); void gtksfv_list_store_add(struct gtksfv_entry_t *p); void gtksfv_file_selection(const gchar *text, gint type, GCallback cb); void gtksfv_file_selection_save(GtkWidget *widget, GtkWidget *fs); void gtksfv_file_selection_open(GtkWidget *widget, GtkWidget *fs); void gtksfv_file_selection_select(GtkWidget *widget, GtkWidget *fs); void gtksfv_window_display_error(const gchar *head, const gchar *fmt, ...); void gtksfv_window_display_notice(const gchar *head, const gchar *fmt, ...); void gtksfv_window_display_about(void); gboolean gtksfv_window_yes_no_prompt(const gchar *fmt, ...); void gtksfv_guess_sfv_type(const gchar *file); const gchar *gtksfv_handle_path(const gchar *file); void gtksfv_perror(const gchar *fmt, ...); void gtksfv_error(const gchar *fmt, ...); #define PERROR gtksfv_perror #define ERROR gtksfv_error gchar *gtksfv_GetFileCRC(); gchar *gtksfv_MD5_File(); gchar *gtksfv_rc; /* path to configuration file */ gboolean gtksfv_opt_remember_last_dir; /* option: remember last directory? */ gchar *gtksfv_opt_last_dir; /* option: last directory */ gboolean gtksfv_opt_prompt_on_unsaved; /* option: prompt for unsaved sfv? */ gint gtksfv_type; /* crc type */ gboolean gtksfv_unsaved; /* unsaved sfv (prompts) */ GtkWidget *gtksfv_window; /* app window; update title */ GtkListStore *gtksfv_list; /* main list for file names and crcs */ struct gtksfv_sb_t *gtksfv_sb; /* main status bar */ struct gtksfv_sb_t *gtksfv_sb_o; /* Ok status bar */ struct gtksfv_sb_t *gtksfv_sb_b; /* Bad status bar */ struct gtksfv_sb_t *gtksfv_sb_m; /* Missing status bar */ gint main(gint argc, gchar **argv) { GtkWidget *vbox; GtkWidget *hbox; GtkWidget *menubar; GtkWidget *scrolled_window; gtk_init(&argc, &argv); gtksfv_rc = g_build_filename(g_get_home_dir(), _PATH_RC, NULL); gtksfv_load_config(); if(gtksfv_opt_remember_last_dir && gtksfv_opt_last_dir) if(IS_DIR(gtksfv_opt_last_dir)) if(chdir(gtksfv_opt_last_dir) == -1) PERROR("main: chdir: %s", gtksfv_opt_last_dir); /* reset type */ gtksfv_type = SFV_NONE; gtksfv_unsaved = FALSE; gtksfv_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); /* set the default title (GtkSFV) */ gtksfv_window_set_title(NULL); g_signal_connect(G_OBJECT(gtksfv_window), "destroy", G_CALLBACK(gtk_main_quit), NULL); g_signal_connect(G_OBJECT(gtksfv_window), "event", G_CALLBACK(gtksfv_window_event_handler), gtksfv_window); gtk_widget_set_usize(GTK_WIDGET(gtksfv_window), 475, 325); gtk_container_set_border_width(GTK_CONTAINER(gtksfv_window), 1); vbox = gtk_vbox_new(FALSE, 3); gtk_widget_show(vbox); gtk_container_add(GTK_CONTAINER(gtksfv_window), vbox); menubar = gtksfv_create_menu(); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(menubar), FALSE, FALSE, 0); gtk_widget_show(menubar); scrolled_window = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_widget_show(scrolled_window); gtk_box_pack_start(GTK_BOX(vbox), scrolled_window, TRUE, TRUE, 0); gtksfv_list = gtksfv_list_store_create(scrolled_window); hbox = gtk_hbox_new(FALSE, 3); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0); gtk_widget_show(hbox); /* create statusbars */ gtksfv_sb = gtksfv_create_statusbar("Status"); gtksfv_sb_o = gtksfv_create_statusbar("Ok"); gtksfv_sb_b = gtksfv_create_statusbar("Bad"); gtksfv_sb_m = gtksfv_create_statusbar("Missing"); /* reset to default values */ gtksfv_reset_statusbars(); /* setup the main statusbar with a longer width for status and other info */ gtk_widget_set_usize(gtksfv_sb->widget, 100, 0); gtk_box_pack_start(GTK_BOX(hbox), gtksfv_sb->widget, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(hbox), gtksfv_sb_o->widget, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(hbox), gtksfv_sb_b->widget, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(hbox), gtksfv_sb_m->widget, TRUE, TRUE, 0); gtk_widget_show(gtksfv_window); /* save configuration when exiting */ g_atexit(gtksfv_exit); /* process file specified on the command line (if any) */ if(argv[1] != NULL) gtksfv_process_sfv(argv[1]); gtk_main(); exit(0); } void gtksfv_open(void) { gtksfv_file_selection("Open sfv/md5", SFV_FILEDIALOG_OPEN, (GCallback) gtksfv_file_selection_open); } void gtksfv_new_sfv(void) { gtksfv_type = SFV_SFV; gtksfv_file_selection("Select file(s) for new sfv", SFV_FILEDIALOG_NEW, (GCallback) gtksfv_file_selection_select); } void gtksfv_new_md5(void) { gtksfv_type = SFV_MD5; gtksfv_file_selection("Select file(s) for new md5", SFV_FILEDIALOG_NEW, (GCallback) gtksfv_file_selection_select); } void gtksfv_save(void) { gchar *text; /* File -> New -> (SFV | MD5) not used */ if(gtksfv_type == SFV_NONE) { gtksfv_window_display_error("Nothing selected.", "No files have been selected for processing (File -> New)."); return; } if(gtksfv_type == SFV_SFV) text = "Save sfv as ..."; else if(gtksfv_type == SFV_MD5) text = "Save md5 as ..."; gtksfv_file_selection(text, SFV_FILEDIALOG_SAVE, (GCallback) gtksfv_file_selection_save); } void gtksfv_quit(void) { /* prompt for unsaved sfv */ if(gtksfv_close_sfv() == FALSE) return; exit(0); } /* for g_atexit() */ void gtksfv_exit(void) { gtksfv_save_config(); g_free(gtksfv_rc); g_free(gtksfv_sb); g_free(gtksfv_sb_o); g_free(gtksfv_sb_b); g_free(gtksfv_sb_m); } void gtksfv_about(void) { gtksfv_window_display_about(); } /* set defaults and process config file */ void gtksfv_load_config(void) { gchar buf[LINE_MAX]; FILE *fp; gtksfv_opt_last_dir = NULL; gtksfv_opt_remember_last_dir = FALSE; gtksfv_opt_prompt_on_unsaved = TRUE; if(IS_FILE(gtksfv_rc) == FALSE) return; if((fp = fopen(gtksfv_rc, "r")) == NULL) { PERROR("gtksfv_load_config: fopen: %s", gtksfv_rc); return; } while((fgets(buf, sizeof(buf), fp))) { gchar **p, *name, *value; buf[strlen(buf) - 1] = 0; if(buf[0] == '\0' || buf[0] == '#' || buf[0] == ' ') continue; if((p = g_strsplit(buf, "=", 2)) == NULL) continue; name = g_strdup(g_strstrip(p[0])); value = g_strdup(g_strstrip(p[1])); g_strfreev(p); if(name == NULL || value == NULL || value[0] == '\0') continue; if(STREQ(name, "remember_last_dir")) gtksfv_opt_remember_last_dir = (IS_TRUE(value)) ? TRUE : FALSE; else if(STREQ(name, "last_dir")) gtksfv_opt_last_dir = g_strdup(value); else if(STREQ(name, "prompt_on_unsaved")) gtksfv_opt_prompt_on_unsaved = (IS_TRUE(value)) ? TRUE : FALSE; else ERROR("skipping unknown configuration directive '%s'", name); g_free(name); g_free(value); } fclose(fp); } /* save config */ void gtksfv_save_config(void) { FILE *fp; if((fp = fopen(gtksfv_rc, "w")) == NULL) { PERROR("gtksfv_save_config: fopen: %s", gtksfv_rc); return; } fprintf(fp, "# GtkSFV v%s\n", GTKSFV_VERSION); fprintf(fp, "# %s: automatically generated, do not edit.\n\n", gtksfv_rc); fprintf(fp, "remember_last_dir = %s\n", (gtksfv_opt_remember_last_dir == TRUE) ? "true" : "false"); gtksfv_opt_last_dir = g_get_current_dir(); if(gtksfv_opt_last_dir && gtksfv_opt_remember_last_dir == TRUE) fprintf(fp, "last_dir = %s\n", gtksfv_opt_last_dir); fprintf(fp, "prompt_on_unsaved = %s\n", (gtksfv_opt_prompt_on_unsaved == TRUE) ? "true" : "false"); g_free(gtksfv_opt_last_dir); fclose(fp); } /* update the window title to show 'GtkSFV: ' */ void gtksfv_window_set_title(const gchar *name) { gchar *text; if(name != NULL) text = g_strdup_printf("GtkSFV: %s", gtksfv_handle_path(name)); else text = g_strdup("GtkSFV"); gtk_window_set_title(GTK_WINDOW(gtksfv_window), text); g_free(text); } #define _GTK_SFV_MARKUP "%s" /* wrapper function to bold labels */ void gtksfv_label_set_markup(GtkLabel *label, const gchar *text) { gchar *buf = g_strdup_printf(_GTK_SFV_MARKUP, text); gtk_label_set_markup(label, buf); g_free(buf); } /* wrapper function to allow labels to use stdarg */ GtkWidget *gtksfv_label_new(const gchar *fmt, ...) { gchar buf[LINE_MAX]; va_list args; va_start(args, fmt); g_vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); return(gtk_label_new(buf)); } /* check received events for , if found, close the window */ gint gtksfv_window_event_handler(GtkWidget *widget, GdkEvent *event, GtkWidget *window) { switch(event->type) { case GDK_KEY_PRESS: switch(event->key.keyval) { case GDK_Escape: gtk_widget_destroy(GTK_WIDGET(window)); break; default: break; } break; default: break; } return(0); } /* return the created main list widget (where checksum data is displayed) */ GtkListStore *gtksfv_list_store_create(GtkWidget *widget) { GtkWidget *tree_view; GtkListStore *store; GtkCellRenderer *renderer; GtkTreeViewColumn *column; store = gtk_list_store_new(SFV_N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); gtk_widget_show(tree_view); gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(tree_view), TRUE); gtk_container_add(GTK_CONTAINER(widget), tree_view); g_object_unref(G_OBJECT(store)); renderer = gtk_cell_renderer_text_new(); g_object_set_data(G_OBJECT(renderer), "column", (gint *) SFV_NAME); column = gtk_tree_view_column_new_with_attributes("File", renderer, "text", SFV_NAME, NULL); gtk_tree_view_column_set_resizable(column, TRUE); gtk_tree_view_column_set_min_width(column, 150); gtk_tree_view_column_set_sort_column_id(column, 0); gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column); renderer = gtk_cell_renderer_text_new(); g_object_set_data(G_OBJECT(renderer), "column", (gint *) SFV_STATUS); column = gtk_tree_view_column_new_with_attributes("Status", renderer, "text", SFV_STATUS, NULL); gtk_tree_view_column_set_resizable(column, TRUE); gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_column_set_sort_column_id(column, 1); gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column); column = gtk_tree_view_column_new_with_attributes("Stored", renderer, "text", SFV_STORED_CRC, NULL); gtk_tree_view_column_set_resizable(column, TRUE); gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column); column = gtk_tree_view_column_new_with_attributes("Local", renderer, "text", SFV_LOCAL_CRC, NULL); gtk_tree_view_column_set_resizable(column, TRUE); gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column); return(store); } /* handle the Remember Last Directory toggle option */ void gtksfv_opt_menu_last(gpointer cb_data, guint cb_action, GtkWidget *item) { gtksfv_opt_remember_last_dir = GTK_CHECK_MENU_ITEM(item)->active; } void gtksfv_opt_menu_prompt(gpointer cb_data, guint cb_action, GtkWidget *item) { gtksfv_opt_prompt_on_unsaved = GTK_CHECK_MENU_ITEM(item)->active; } #define _OPT_MENU_LAST "/Options/Remember Last Directory" #define _OPT_MENU_PROMPT "/Options/Prompt On Unsaved SFVs" GtkItemFactoryEntry menu_items[] = { { "/_File", NULL, NULL, 0, "" }, { "/File/Open", NULL, gtksfv_open, 0, "", GTK_STOCK_OPEN }, { "/File/sep", NULL, NULL, 0, "" }, { "/File/New/SFV", NULL, gtksfv_new_sfv, 0, "" }, { "/File/New/MD5", NULL, gtksfv_new_md5, 0, "" }, { "/File/Save", NULL, gtksfv_save, 0, "", GTK_STOCK_SAVE }, { "/File/sep", NULL, NULL, 0, "" }, { "/File/Quit", NULL, gtksfv_quit, 0, "", GTK_STOCK_QUIT }, { "/_Options", NULL, NULL, 0, "" }, { _OPT_MENU_LAST, NULL, gtksfv_opt_menu_last, 0, "" }, { _OPT_MENU_PROMPT, NULL, gtksfv_opt_menu_prompt, 0, "" }, { "/_Help", NULL, NULL, 0, "" }, { "/Help/About", NULL, gtksfv_about, 0, "" } }; /* return the GtkItemFactory created menu */ GtkWidget *gtksfv_create_menu(void) { GtkItemFactory *item_factory; item_factory = gtk_item_factory_new(GTK_TYPE_MENU_BAR, "
", NULL); gtk_item_factory_create_items(item_factory, G_N_ELEMENTS(menu_items), menu_items, NULL); gtk_check_menu_item_set_active( GTK_CHECK_MENU_ITEM( gtk_item_factory_get_item(item_factory, _OPT_MENU_LAST) ), gtksfv_opt_remember_last_dir); gtk_check_menu_item_set_active( GTK_CHECK_MENU_ITEM( gtk_item_factory_get_item(item_factory, _OPT_MENU_PROMPT) ), gtksfv_opt_prompt_on_unsaved); return(gtk_item_factory_get_widget(item_factory, "
")); } /* returns created statusbar widgets */ struct gtksfv_sb_t *gtksfv_create_statusbar(const gchar *desc) { struct gtksfv_sb_t *sb; g_return_val_if_fail(desc != NULL && desc[0] != '\0', NULL); sb = g_new(struct gtksfv_sb_t, 1); sb->desc = g_strdup(desc); /* for statusbar updates (desc: ) */ sb->widget = gtk_statusbar_new(); sb->cid = gtk_statusbar_get_context_id(GTK_STATUSBAR(sb->widget), desc); gtk_widget_show(sb->widget); return(sb); } /* reset statusbars to their default values (no open sfv) */ void gtksfv_reset_statusbars(void) { gtksfv_statusbar_push(gtksfv_sb, "Ready"); gtksfv_statusbar_push(gtksfv_sb_o, "-"); gtksfv_statusbar_push(gtksfv_sb_b, "-"); gtksfv_statusbar_push(gtksfv_sb_m, "-"); } /* update the statsbar */ void gtksfv_statusbar_push(struct gtksfv_sb_t *sb, const gchar *fmt, ...) { gchar buf[LINE_MAX], *text; va_list args; va_start(args, fmt); g_vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); text = g_strdup_printf("%s: %s", sb->desc, buf); gtk_statusbar_pop(GTK_STATUSBAR(sb->widget), sb->cid); gtk_statusbar_push(GTK_STATUSBAR(sb->widget), sb->cid, text); g_free(text); } /* saves the sfv */ gboolean gtksfv_save_sfv(const gchar *file) { const gchar *path; GSList *list = NULL; FILE *fp; if(file == NULL || file[0] == '\0') return(FALSE); path = gtksfv_handle_path(file); /* file already exists, display an overwrite prompt */ if(IS_FILE(path) == TRUE) if(gtksfv_window_yes_no_prompt("%s exists, overwrite?", path) == FALSE) return(FALSE); /* no valid files found, display an error and close the file dialog */ if((list = gtksfv_get_list_store()) == NULL) { gtksfv_window_display_error("Nothing to process.", "No (valid) files in the list. Select one or more (File -> New), then try again."); return(TRUE); } if((fp = fopen(file, "wt")) == NULL) { gtksfv_window_display_error("Error saving gtksfv_", "Unable to open %s: %s", path, G_ERRMSG); g_slist_foreach(list, (GFunc) g_free, NULL); g_slist_free(list); return(FALSE); } /* write file header information (version info, date, time - if sfv) */ gtksfv_write_sfv_header(fp); /* write any file comments (size, modification time - if sfv */ g_slist_foreach(list, (GFunc) gtksfv_write_sfv_comment, fp); /* write checkum values */ g_slist_foreach(list, (GFunc) gtksfv_write_sfv_line, fp); fclose(fp); /* free list data */ g_slist_foreach(list, (GFunc) g_free, NULL); g_slist_free(list); /* update window title to show we are working with a saved sfv */ gtksfv_window_set_title(file); return(TRUE); } /* writes file header information (version info, date, time - if sfv) */ void gtksfv_write_sfv_header(FILE *fp) { time_t time_base; g_return_if_fail(fp != NULL); if(gtksfv_type != SFV_SFV) return; time_base = time(NULL); /* ; Generated by GtkSFV/0.02 on Tue Jul 22 12:09:37 2003 */ fprintf(fp, "; Generated by GtkSFV/%s on %s;\n", GTKSFV_VERSION, ctime(&time_base)); } /* writes any file comments (size, modification time - if sfv */ void gtksfv_write_sfv_comment(struct gtksfv_entry_t *e, FILE *fp) { gchar date[PATH_MAX]; struct stat statbuf; g_return_if_fail(fp != NULL); g_return_if_fail(e != NULL); if(gtksfv_type != SFV_SFV) return; /* file does not exist, add anyway with empty values */ if(stat(e->file, &statbuf)) { statbuf.st_mtime = 0; statbuf.st_size = 0; } g_strlcpy(date, ctime(&statbuf.st_mtime), sizeof(date)); /* not sure why ctime() appends a newline */ date[strlen(date) - 1] = 0; /* ; 292979 Tue Jul 22 12:09:24 2003 GtkSFV */ fprintf(fp, "; %10lu %s %s\n", statbuf.st_size, date, e->file); } /* write checkum values */ void gtksfv_write_sfv_line(struct gtksfv_entry_t *e, FILE *fp) { g_return_if_fail(fp != NULL); g_return_if_fail(e != NULL); if(gtksfv_type == SFV_NONE) return; /* * I rather like the BSD (?) style md5sum, but GNU text/coreutils md5sum * doesn't know how to -c that, so I'll use the "regular" style. */ if(gtksfv_type == SFV_SFV) fprintf(fp, "%s %s\n", e->file, e->local_crc); else if(gtksfv_type == SFV_MD5) fprintf(fp, "%s %s\n", e->local_crc, e->file); } /* displays a prompt asking to close unsaved sfv, return TRUE on Yes */ gboolean gtksfv_close_sfv(void) { if(gtksfv_opt_prompt_on_unsaved == FALSE || gtksfv_unsaved == FALSE) return(TRUE); return(gtksfv_window_yes_no_prompt("Close unsaved sfv/md5?")); } /* wrapper for processing the checksum file */ gboolean gtksfv_process_sfv(const gchar *file) { g_return_val_if_fail(file != NULL && file[0] != '\0', FALSE); gtksfv_list_store_clear(); gtksfv_guess_sfv_type(file); if(gtksfv_type == SFV_NONE) { gtksfv_statusbar_push(gtksfv_sb, "Unknown file type"); return(FALSE); } if(IS_FILE(file) == FALSE) { gtksfv_statusbar_push(gtksfv_sb, "Unable to process"); return(FALSE); } gtksfv_statusbar_push(gtksfv_sb, "Processing ..."); /* process the checksum file */ if(gtksfv_check_sfv_file(file) == FALSE) { gtksfv_list_store_clear(); return(FALSE); } gtksfv_window_set_title(gtksfv_handle_path(file)); gtksfv_unsaved = FALSE; return(TRUE); } /* process the checksum file. returns FALSE on file open errors */ gboolean gtksfv_check_sfv_file(const gchar *file) { gchar buf[LINE_MAX]; gchar *dirname; guint count_o = 0, count_m = 0, count_b = 0, count_t = 0; FILE *fp; g_return_val_if_fail(file != NULL && file[0] != '\0', FALSE); if(IS_FILE(file) == FALSE) return(FALSE); if((fp = fopen(file, "rt")) == NULL) { gtksfv_window_display_error("Error opening gtksfv_", "Unable to open %s: %s", gtksfv_handle_path(file), G_ERRMSG); return(FALSE); } /* * I'm not quite sure what to do here, feels like I'm making this too * complicated... If started from a menu or command line, I think it * would be best to chdir() to the location of the sfv file. * * Of course this is no good if all the files happen to be in the current * directory and the sfv in another, but for most situations this work * alright... I hope. :) */ dirname = g_path_get_dirname(file); if(IS_DIR(dirname) == TRUE) if(chdir(dirname) == -1) PERROR("gtksfv_check_sfv_file: chdir: %s", dirname); g_free(dirname); while((fgets(buf, sizeof(buf), fp))) { gchar *path; struct gtksfv_entry_t *e; buf[strlen(buf) - 1] = 0; /* no valid entries found */ if((e = gtksfv_parse_sfv_line(g_strstrip(buf))) == NULL) continue; count_t++; path = gtksfv_resolve_sfv_file(e->file); if(IS_EXIST(path) == FALSE) { e->status = _SFV_ST_MISSING; count_m++; } else { gchar *crc; /* crc returned, compare */ if((crc = gtksfv_get_file_crc(e->file))) { g_strlcpy(e->local_crc, crc, sizeof(e->local_crc)); g_free(crc); /* crcs match */ if(STREQ(e->stored_crc, e->local_crc)) { count_o++; e->status = _SFV_ST_OK; } else { count_b++; e->status = _SFV_ST_BAD; } } else { count_b++; e->status = _SFV_ST_ERROR; } } g_free(path); gtksfv_list_store_add(e); g_free(e); } fclose(fp); if(!count_t) { gtksfv_window_display_notice("Nothing to process.", "No valid entries found, is %s a valid sfv/md5 checksum file?", gtksfv_handle_path(file)); gtksfv_list_store_clear(); return(FALSE); } /* update the titlebars with gathered stats */ gtksfv_update_sfv_statusbars(count_o, count_m, count_b, count_t); return(TRUE); } /* update statusbars with stats after processing */ void gtksfv_update_sfv_statusbars(guint ok, guint missing, guint bad, guint total) { if(total == ok) gtksfv_statusbar_push(gtksfv_sb, "Ok"); else if(missing && !bad) gtksfv_statusbar_push(gtksfv_sb, "Incomplete"); else gtksfv_statusbar_push(gtksfv_sb, "Bad"); gtksfv_statusbar_push(gtksfv_sb_o, "%u", ok); gtksfv_statusbar_push(gtksfv_sb_m, "%u", missing); gtksfv_statusbar_push(gtksfv_sb_b, "%u", bad); } /* * attempt to match checksum files created on Windows, checking for all * lower, upper, or first char upper case. mimics the pdsfv behavior. */ gchar *gtksfv_resolve_sfv_file(const gchar *file) { gchar *path; if(IS_EXIST(file) == TRUE) return(g_strdup(file)); path = g_ascii_strup(file, -1); if(IS_EXIST(path) == TRUE) return(path); g_free(path); path = g_ascii_strdown(file, -1); if(IS_EXIST(path) == TRUE) return(path); g_free(path); path = g_strdup(file); path[0] = g_ascii_toupper(path[0]); if(IS_EXIST(path) == TRUE) return(path); g_free(path); return(g_strdup(file)); } /* parse checksum file entry, returns null on invalid line */ struct gtksfv_entry_t *gtksfv_parse_sfv_line(const gchar *line) { gint line_len; struct gtksfv_entry_t *e; g_return_val_if_fail(line != NULL, NULL); if(line[0] == '\0' || line[0] == ';' || line[0] == '#' || line[0] == ' ' || strchr(line, ' ') == NULL) return(NULL); line_len = strlen(line); if(gtksfv_type == SFV_NONE) return(NULL); e = gtksfv_entry_new(); if(gtksfv_type == SFV_SFV) { /* * I've yet to see some really mal-formed sfv files, but it would be * easy to screw these parsing routines up... could use some work. */ g_strlcpy(e->file, line, line_len - _SFV_SFV_LEN); /* crc = strtoul(buf, NULL, 16); */ sscanf(line + (line_len - _SFV_SFV_LEN), "%8s", e->stored_crc); /* ignore non-sfv entries */ if(strlen(e->stored_crc) != _SFV_SFV_LEN) { g_free(e); return(NULL); } } else if(gtksfv_type == SFV_MD5) { /* these are better, but still not that great.. :I */ if(STRNEQ(line, "MD5 (", 5)) sscanf(line, "MD5 (%1024[^)]) = %32s", e->file, e->stored_crc); else sscanf(line, "%32s %1024[^\n]", e->stored_crc, e->file); /* ignore non-md5 entries (like PGP signatures) */ if(strlen(e->stored_crc) != _SFV_MD5_LEN) { g_free(e); return(NULL); } } return(e); } /* return the crc for the file */ gchar *gtksfv_get_file_crc(const gchar *file) { gchar *crc; g_return_val_if_fail(file != NULL && file[0] != '\0', NULL); if(IS_EXIST(file) == FALSE || gtksfv_type == SFV_NONE) return(NULL); if(gtksfv_type == SFV_SFV) crc = gtksfv_GetFileCRC(file); else if(gtksfv_type == SFV_MD5) crc = gtksfv_MD5_File(file); return(crc); } struct gtksfv_entry_t *gtksfv_entry_new(void) { struct gtksfv_entry_t *e; e = g_new(struct gtksfv_entry_t, 1); e->file[0] = 0; e->local_crc[0] = 0; e->stored_crc[0] = 0; e->status = NULL; return(e); } /* clear the main list, reset statusbars and the window title */ void gtksfv_list_store_clear(void) { gtk_list_store_clear((GtkListStore *) gtksfv_list); gtksfv_reset_statusbars(); gtksfv_window_set_title(NULL); } /* returns a GSList of checksum data contained in the list (for saving) */ GSList *gtksfv_get_list_store(void) { GSList *list = NULL; GtkTreeModel *model = (GtkTreeModel *) gtksfv_list; GtkTreeIter iter; /* nothing in the list */ if(gtk_tree_model_get_iter_first(model, &iter) == FALSE) return(NULL); do { gchar *file, *local_crc, *status; struct gtksfv_entry_t *e; gtk_tree_model_get(model, &iter, SFV_NAME, &file, SFV_STATUS, &status, SFV_LOCAL_CRC, &local_crc, -1); /* * is it common for people to re-save sfv's with bad or missing * files? hope not with bad, but missing I can see... */ if(!STREQ(_SFV_ST_OK, status)) { g_free(file); g_free(status); g_free(local_crc); continue; } e = gtksfv_entry_new(); g_strlcpy(e->file, file, sizeof(e->file)); g_strlcpy(e->local_crc, local_crc, sizeof(e->local_crc)); g_free(file); g_free(status); g_free(local_crc); list = g_slist_append(list, e); } while(gtk_tree_model_iter_next(model, &iter)); return(list); } /* add sfv entry to the main checksum list */ void gtksfv_list_store_add(struct gtksfv_entry_t *e) { GtkListStore *list = (GtkListStore *) gtksfv_list; GtkTreeIter iter; g_return_if_fail(e != NULL); gtk_list_store_append(GTK_LIST_STORE(list), &iter); gtk_list_store_set(GTK_LIST_STORE(list), &iter, SFV_NAME, gtksfv_handle_path(e->file), SFV_STATUS, e->status, SFV_STORED_CRC, e->stored_crc, SFV_LOCAL_CRC, e->local_crc, -1); } /* create a GtkFileSelection dialog */ void gtksfv_file_selection(const gchar *text, gint type, GCallback cb) { GtkWidget *fs; g_return_if_fail(text != NULL && text[0] != '\0'); if(type != SFV_FILEDIALOG_SAVE) /* don't prompt when saving */ if(gtksfv_close_sfv() == FALSE) return; fs = gtk_file_selection_new(text); /* selecting files for new sfv/md5 creation */ if(type == SFV_FILEDIALOG_NEW) gtk_file_selection_set_select_multiple(GTK_FILE_SELECTION(fs), TRUE); g_signal_connect(G_OBJECT(GTK_FILE_SELECTION(fs)->ok_button), "clicked", G_CALLBACK(cb), (gpointer) fs); g_signal_connect_swapped(G_OBJECT(GTK_FILE_SELECTION(fs)->cancel_button), "clicked", G_CALLBACK(gtk_widget_destroy), G_OBJECT(fs)); gtk_widget_show(fs); } /* retrieve filename from GtkFileSelection and save newly created sfv/md5 */ void gtksfv_file_selection_save(GtkWidget *widget, GtkWidget *fs) { const gchar *file; if((file = gtk_file_selection_get_filename(GTK_FILE_SELECTION(fs))) == NULL) return; if(strlen(file) && IS_DIR(file) == FALSE) { if(gtksfv_save_sfv(file) == FALSE) return; } else { gtksfv_window_display_error("Target must be a file.", "The selected target item is not a file."); gtksfv_statusbar_push(gtksfv_sb, "Unable to save"); return; } gtk_widget_destroy(fs); gtksfv_unsaved = FALSE; } /* retrieve filename from GtkFileSelection and open sfv/md5 */ void gtksfv_file_selection_open(GtkWidget *widget, GtkWidget *fs) { const gchar *file; if((file = gtk_file_selection_get_filename(GTK_FILE_SELECTION(fs))) == NULL) return; if(strlen(file) && IS_DIR(file) == FALSE) { if(gtksfv_process_sfv(file) == FALSE) return; } else { gtksfv_window_display_error("Source must be a file.", "The selected source item is not a file."); gtksfv_statusbar_push(gtksfv_sb, "Unable to process"); return; } gtk_widget_destroy(fs); } /* retrieve selected file(s) from GtkFileSelection and generate checksums */ void gtksfv_file_selection_select(GtkWidget *widget, GtkWidget *fs) { guint count_o = 0, count_m = 0, count_b = 0, count_t = 0; const gchar *path; gchar **files; gtksfv_list_store_clear(); if(gtksfv_type == SFV_NONE) return; if(gtksfv_type == SFV_SFV) gtksfv_window_set_title(""); else if(gtksfv_type == SFV_MD5) gtksfv_window_set_title(""); files = gtk_file_selection_get_selections(GTK_FILE_SELECTION(fs)); if(files == NULL) { gtksfv_list_store_clear(); gtk_widget_destroy(fs); return; } path = gtksfv_handle_path(files[0]); /* change to the directory of the files being processed */ if(path[0] == '/' || strchr(path, '/')) { gchar *dirname = g_path_get_dirname(path); if(IS_DIR(dirname) == TRUE) if(chdir(dirname) == -1) PERROR("gtksfv_file_selection_select: chdir: %s", dirname); g_free(dirname); } /* process each selected file */ while(files[count_t]) { struct gtksfv_entry_t *e = gtksfv_entry_new(); g_strlcpy(e->file, gtksfv_handle_path(files[count_t]), sizeof(e->file)); count_t++; if(IS_EXIST(e->file) == FALSE) { e->status = _SFV_ST_MISSING; count_m++; } else { gchar *crc; if((crc = gtksfv_get_file_crc(e->file))) { g_strlcpy(e->local_crc, crc, sizeof(e->local_crc)); g_free(crc); count_o++; e->status = _SFV_ST_OK; } else { count_b++; e->status = _SFV_ST_ERROR; } } gtksfv_list_store_add(e); g_free(e); } g_strfreev(files); gtksfv_update_sfv_statusbars(count_o, count_m, count_b, count_t); gtk_widget_destroy(fs); gtksfv_unsaved = TRUE; } /* display an error message */ void gtksfv_window_display_error(const gchar *head, const gchar *fmt, ...) { GtkWidget *window; GtkWidget *vbox; GtkWidget *vbox2; GtkWidget *hbox; GtkWidget *image; GtkWidget *label; GtkWidget *button; gchar buf[LINE_MAX]; va_list args; g_return_if_fail(head != NULL && head[0] != '\0'); va_start(args, fmt); g_vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "Error"); gtk_window_set_policy(GTK_WINDOW(window), FALSE, FALSE, FALSE); gtk_widget_set_usize(GTK_WIDGET(window), 425, 0); g_signal_connect(G_OBJECT(window), "event", G_CALLBACK(gtksfv_window_event_handler), window); gtk_container_set_border_width(GTK_CONTAINER(window), 5); vbox = gtk_vbox_new(FALSE, 5); gtk_widget_show(vbox); gtk_container_add(GTK_CONTAINER(window), vbox); hbox = gtk_hbox_new(FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); gtk_widget_show(hbox); image = gtk_image_new_from_stock(GTK_STOCK_DIALOG_ERROR, GTK_ICON_SIZE_DIALOG); gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0); gtk_widget_show(image); gtk_misc_set_alignment(GTK_MISC(image), 0.5, 0.0); gtk_misc_set_padding(GTK_MISC(image), 0, 5); vbox2 = gtk_vbox_new(FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox), vbox2, FALSE, FALSE, 0); gtk_widget_show(vbox2); gtk_container_set_border_width(GTK_CONTAINER(vbox2), 5); label = gtk_label_new(NULL); gtk_box_pack_start(GTK_BOX(vbox2), label, TRUE, FALSE, 5); gtk_widget_show(label); gtk_widget_set_usize(GTK_WIDGET(label), 350, 0); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); gtksfv_label_set_markup(GTK_LABEL(label), head); label = gtk_label_new(buf); gtk_box_pack_start(GTK_BOX(vbox2), label, TRUE, FALSE, 0); gtk_widget_show(label); gtk_widget_set_usize(GTK_WIDGET(label), 350, 0); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); button = gtk_button_new_from_stock(GTK_STOCK_CLOSE); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); gtk_widget_show(button); g_signal_connect_swapped(G_OBJECT(button), "clicked", G_CALLBACK(gtk_widget_destroy), G_OBJECT(window)); gtk_widget_show(window); } /* display a notification */ void gtksfv_window_display_notice(const gchar *head, const gchar *fmt, ...) { GtkWidget *window; GtkWidget *vbox; GtkWidget *vbox2; GtkWidget *hbox; GtkWidget *image; GtkWidget *label; GtkWidget *button; gchar buf[LINE_MAX]; va_list args; g_return_if_fail(head != NULL && head[0] != '\0'); va_start(args, fmt); g_vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "Notice"); gtk_window_set_policy(GTK_WINDOW(window), FALSE, FALSE, FALSE); gtk_widget_set_usize(GTK_WIDGET(window), 425, 0); g_signal_connect(G_OBJECT(window), "event", G_CALLBACK(gtksfv_window_event_handler), window); gtk_container_set_border_width(GTK_CONTAINER(window), 5); vbox = gtk_vbox_new(FALSE, 5); gtk_widget_show(vbox); gtk_container_add(GTK_CONTAINER(window), vbox); hbox = gtk_hbox_new(FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); gtk_widget_show(hbox); image = gtk_image_new_from_stock(GTK_STOCK_DIALOG_WARNING, GTK_ICON_SIZE_DIALOG); gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0); gtk_widget_show(image); gtk_misc_set_alignment(GTK_MISC(image), 0.5, 0.0); gtk_misc_set_padding(GTK_MISC(image), 0, 5); vbox2 = gtk_vbox_new(FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox), vbox2, FALSE, FALSE, 0); gtk_widget_show(vbox2); gtk_container_set_border_width(GTK_CONTAINER(vbox2), 5); label = gtk_label_new(NULL); gtk_box_pack_start(GTK_BOX(vbox2), label, TRUE, FALSE, 5); gtk_widget_show(label); gtk_widget_set_usize(GTK_WIDGET(label), 350, 0); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); gtksfv_label_set_markup(GTK_LABEL(label), head); label = gtk_label_new(buf); gtk_box_pack_start(GTK_BOX(vbox2), label, TRUE, FALSE, 0); gtk_widget_show(label); gtk_widget_set_usize(GTK_WIDGET(label), 350, 0); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); button = gtk_button_new_from_stock(GTK_STOCK_CLOSE); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); gtk_widget_show(button); g_signal_connect_swapped(G_OBJECT(button), "clicked", G_CALLBACK(gtk_widget_destroy), G_OBJECT(window)); gtk_widget_show(window); } /* display about window */ void gtksfv_window_display_about(void) { GtkWidget *window; GtkWidget *vbox; GtkWidget *vbox2; GtkWidget *hbox; GtkWidget *image; GtkWidget *label; GtkWidget *button; const gchar COPYRIGHT[3] = { 0xc2, 0xa9, 0 }; /* copyright symbol */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "About"); gtk_window_set_policy(GTK_WINDOW(window), FALSE, FALSE, FALSE); g_signal_connect(G_OBJECT(window), "event", G_CALLBACK(gtksfv_window_event_handler), window); gtk_container_set_border_width(GTK_CONTAINER(window), 5); vbox = gtk_vbox_new(FALSE, 5); gtk_widget_show(vbox); gtk_container_add(GTK_CONTAINER(window), vbox); hbox = gtk_hbox_new(FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); gtk_widget_show(hbox); image = gtk_image_new_from_stock(GTK_STOCK_DIALOG_INFO, GTK_ICON_SIZE_DIALOG); gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0); gtk_widget_show(image); gtk_misc_set_alignment(GTK_MISC(image), 0.5, 0.0); gtk_misc_set_padding(GTK_MISC(image), 0, 5); vbox2 = gtk_vbox_new(FALSE, 2); gtk_widget_show(vbox2); gtk_container_set_border_width(GTK_CONTAINER(vbox2), 5); gtk_container_add(GTK_CONTAINER(hbox), vbox2); label = gtk_label_new(NULL); gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_box_pack_start(GTK_BOX(vbox2), label, FALSE, FALSE, 5); gtk_widget_show(label); gtksfv_label_set_markup(GTK_LABEL(label), "GtkSFV v"GTKSFV_VERSION); label = gtk_label_new(GTKSFV_URL); gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_box_pack_start(GTK_BOX(vbox2), label, FALSE, FALSE, 0); gtk_widget_show(label); label = gtksfv_label_new("%s %s", COPYRIGHT, GTKSFV_AUTHOR); gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_box_pack_start(GTK_BOX(vbox2), label, FALSE, FALSE, 0); gtk_widget_show(label); button = gtk_button_new_from_stock(GTK_STOCK_OK); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); gtk_widget_show(button); g_signal_connect_swapped(G_OBJECT(button), "clicked", G_CALLBACK(gtk_widget_destroy), G_OBJECT(window)); gtk_widget_show(window); } /* display a Yes/No dialog prompt. return TRUE on Yes, FALSE on No */ gboolean gtksfv_window_yes_no_prompt(const gchar *fmt, ...) { GtkWidget *dialog; gchar buf[LINE_MAX]; va_list args; va_start(args, fmt); g_vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, buf); switch(gtk_dialog_run(GTK_DIALOG(dialog))) { case GTK_RESPONSE_YES: gtk_widget_destroy(dialog); return(TRUE); default: break; } gtk_widget_destroy(dialog); return(FALSE); } /* * md5sum files seem to come in varying names, at least more than sfv files. * set the type so we know what functions to run on the checksum file. */ void gtksfv_guess_sfv_type(const gchar *file) { if(file == NULL || file[0] == '\0') gtksfv_type = SFV_NONE; else if(strstr(file, ".sfv") || strstr(file, ".SFV")) gtksfv_type = SFV_SFV; else gtksfv_type = SFV_MD5; } /* function that tries to keep paths relative to the CWD */ const gchar *gtksfv_handle_path(const gchar *file) { gchar *dirname, *cwd; gint cwd_len; static gchar path[PATH_MAX]; g_return_val_if_fail(file != NULL && file[0] != '\0', NULL); if(file[0] != '/') return(file); dirname = g_path_get_dirname(file); cwd = g_get_current_dir(); cwd_len = strlen(cwd); /* paths equal (file in CWD), just return filename */ if(STREQ(dirname, cwd)) { gchar *p = g_path_get_basename(file); g_strlcpy(path, p, sizeof(path)); g_free(p); g_free(dirname); g_free(cwd); return(path); } /* file is relative to CWD, return - CWD */ if(STRNEQ(dirname, cwd, cwd_len)) { g_strlcpy(path, file + cwd_len + 1, sizeof(path)); g_free(dirname); g_free(cwd); return(path); } g_free(dirname); g_free(cwd); /* return original, absolute path */ return(file); } /* perror wrapper */ void gtksfv_perror(const gchar *fmt, ...) { gchar buf[LINE_MAX]; va_list args; va_start(args, fmt); g_vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); G_PERROR(buf); } /* fprintf(stderr, ...) wrapper */ void gtksfv_error(const gchar *fmt, ...) { gchar buf[LINE_MAX]; va_list args; va_start(args, fmt); g_vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); fprintf(stderr, "%s: %s\n", g_get_prgname(), buf); } /* * functions take from 'public domain SFV builder for UNiX, build 99.11.10.01' */ glong pdsfv_UpdateCRC(gulong crc, const gchar *buf, glong count) { /* * Note: if you want to know how CRC32-checking works, I * recommend grabbing any old (mid 90īs) Z-Modem source. * There is not much you can change in this function, so * if you need a CRC32-check yourself, feel free to rip. */ gulong CRCTABLE[] = { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d }; if(!buf || !count) return(crc); do { crc = ((crc >> 8) & 0xFFFFFF) ^ CRCTABLE[(guchar)((crc & 0xff) ^ *buf++)]; } while(--count); return(crc); } gchar *gtksfv_GetFileCRC(const gchar *file) { gulong crc = 0xffffffff; FILE *fp; glong bytes; gchar *local_crc; /* * Note: different buffer sizes may result in noticable * different performance depending on system, so feel * free to modify. */ #define BUFFERSIZE (65536 * 16) gchar buf[BUFFERSIZE]; if((fp = fopen(file, "rb")) == NULL) { PERROR("gtksfv_GetFileCRC: fopen: %s", file); return(NULL); } do { if((bytes = fread(buf, 1, sizeof(buf), fp))) crc = pdsfv_UpdateCRC(crc, buf, bytes); } while(bytes > 0); fclose(fp); crc = crc ^ 0xffffffff; local_crc = g_strdup_printf("%08X", (guint) crc); return(local_crc); } /* *********************************************************************** ** md5.c -- the source code for MD5 routines ** ** RSA Data Security, Inc. MD5 Message-Digest Algorithm ** ** Created: 2/17/90 RLR ** ** Revised: 1/91 SRD,AJ,BSK,JT Reference C Version ** *********************************************************************** */ /* * Edited 7 May 93 by CP to change the interface to match that * of the MD5 routines in RSAREF. Due to this alteration, this * code is "derived from the RSA Data Security, Inc. MD5 Message- * Digest Algorithm". (See below.) */ /* *********************************************************************** ** Copyright(C) 1990, RSA Data Security, Inc. All rights reserved. ** ** ** ** License to copy and use this software is granted provided that ** ** it is identified as the "RSA Data Security, Inc. MD5 Message- ** ** Digest Algorithm" in all material mentioning or referencing this ** ** software or this function. ** ** ** ** License is also granted to make and use derivative works ** ** provided that such works are identified as "derived from the RSA ** ** Data Security, Inc. MD5 Message-Digest Algorithm" in all ** ** material mentioning or referencing the derived work. ** ** ** ** RSA Data Security, Inc. makes no representations concerning ** ** either the merchantability of this software or the suitability ** ** of this software for any particular purpose. It is provided "as ** ** is" without express or implied warranty of any kind. ** ** ** ** These notices must be retained in any copies of any part of this ** ** documentation and/or software. ** *********************************************************************** */ gchar *gtksfv_MD5_Digest(); /* Data structure for MD5(Message-Digest) computation */ typedef struct { guint32 buf[4]; /* scratch buffer */ guint32 i[2]; /* number of _bits_ handled mod 2^64 */ guchar in[64]; /* input buffer */ } MD5_CTX; void rsa_MD5_Init(MD5_CTX *ctx); void rsa_MD5_Update(MD5_CTX *ctx, guchar *bug, guint len); void rsa_MD5_Final(guchar digest[16], MD5_CTX *ctx); void rsa_MD5_Transform(guint32 *buf, guint32 *in); /* *********************************************************************** ** Message-digest routines: ** ** To form the message digest for a message M ** ** (1) Initialize a context buffer ctx using MD5_Init ** ** (2) Call MD5_Update on ctx and M ** ** (3) Call MD5_Final on ctx ** ** The message digest is now in the bugffer passed to MD5_Final ** *********************************************************************** */ static guchar PADDING[64] = { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; /* F, G, H and I are basic MD5 functions */ #define F(x, y, z) (((x) & (y)) | ((~x) & (z))) #define G(x, y, z) (((x) & (z)) | ((y) & (~z))) #define H(x, y, z) ((x) ^ (y) ^ (z)) #define I(x, y, z) ((y) ^ ((x) | (~z))) /* ROTATE_LEFT rotates x left n bits */ #define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) /* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4 */ /* Rotation is separate from addition to prevent recomputation */ #define FF(a, b, c, d, x, s, ac) \ { \ (a) += F((b), (c), (d)) + (x) + (guint32) (ac); \ (a) = ROTATE_LEFT((a), (s)); \ (a) += (b); \ } #define GG(a, b, c, d, x, s, ac) \ { \ (a) += G((b), (c), (d)) + (x) + (guint32) (ac); \ (a) = ROTATE_LEFT((a), (s)); \ (a) += (b); \ } #define HH(a, b, c, d, x, s, ac) \ { \ (a) += H((b), (c), (d)) + (x) + (guint32) (ac); \ (a) = ROTATE_LEFT((a), (s)); \ (a) += (b); \ } #define II(a, b, c, d, x, s, ac) \ { \ (a) += I((b), (c), (d)) + (x) + (guint32) (ac); \ (a) = ROTATE_LEFT((a), (s)); \ (a) += (b); \ } /* The routine MD5_Init initializes the message-digest context * ctx. All fields are set to zero. */ void rsa_MD5_Init(MD5_CTX *ctx) { ctx->i[0] = ctx->i[1] = (guint32) 0; /* Load magic initialization constants. */ ctx->buf[0] = (guint32) 0x67452301L; ctx->buf[1] = (guint32) 0xefcdab89L; ctx->buf[2] = (guint32) 0x98badcfeL; ctx->buf[3] = (guint32) 0x10325476L; } /* The routine MD5_Update updates the message-digest context to * account for the presence of each of the characters inBuf[0..inLen-1] * in the message whose digest is being computed. */ void rsa_MD5_Update(register MD5_CTX *ctx, guchar *inBuf, guint inLen) { register gint i, ii; gint mdi; guint32 in[16]; /* compute number of bytes mod 64 */ mdi = (gint) ((ctx->i[0] >> 3) & 0x3F); /* update number of bits */ if((ctx->i[0] + ((guint32) inLen << 3)) < ctx->i[0]) ctx->i[1]++; ctx->i[0] += ((guint32) inLen << 3); ctx->i[1] += ((guint32) inLen >> 29); while(inLen--) { /* add new character to buffer, increment mdi */ ctx->in[mdi++] = *inBuf++; /* transform if necessary */ if(mdi == 0x40) { for(i = 0, ii = 0; i < 16; i++, ii += 4) in[i] = (((guint32) ctx->in[ii + 3]) << 24) | (((guint32) ctx->in[ii + 2]) << 16) | (((guint32) ctx->in[ii + 1]) << 8) | ((guint32) ctx->in[ii]); rsa_MD5_Transform(ctx->buf, in); mdi = 0; } } } /* The routine MD5_Final terminates the message-digest computation and * ends with the desired message digest in ctx->digest[0...15]. */ void rsa_MD5_Final(guchar digest[16], MD5_CTX *ctx) { guint32 in[16]; gint mdi; guint i, ii; guint padLen; /* save number of bits */ in[14] = ctx->i[0]; in[15] = ctx->i[1]; /* compute number of bytes mod 64 */ mdi = (gint) ((ctx->i[0] >> 3) & 0x3F); /* pad out to 56 mod 64 */ padLen = (mdi < 56) ? (56 - mdi) : (120 - mdi); rsa_MD5_Update(ctx, PADDING, padLen); /* append length in bits and transform */ for(i = 0, ii = 0; i < 14; i++, ii += 4) in[i] = (((guint32) ctx->in[ii + 3]) << 24) | (((guint32) ctx->in[ii + 2]) << 16) | (((guint32) ctx->in[ii + 1]) << 8) | ((guint32) ctx->in[ii]); rsa_MD5_Transform(ctx->buf, in); /* store buffer in digest */ for(i = 0, ii = 0; i < 4; i++, ii += 4) { digest[ii] = (guchar) (ctx->buf[i] & 0xFF); digest[ii + 1] = (guchar) ((ctx->buf[i] >> 8) & 0xFF); digest[ii + 2] = (guchar) ((ctx->buf[i] >> 16) & 0xFF); digest[ii + 3] = (guchar) ((ctx->buf[i] >> 24) & 0xFF); } } /* Basic MD5 step. Transforms buf based on in. Note that if the Mysterious * Constants are arranged backwards in little-endian order and decrypted with * the DES they produce OCCULT MESSAGES! */ void rsa_MD5_Transform(register guint32 *buf, register guint32 *in) { register guint32 a = buf[0], b = buf[1], c = buf[2], d = buf[3]; /* Round 1 */ #define S11 7 #define S12 12 #define S13 17 #define S14 22 FF(a, b, c, d, in[0], S11, 0xD76AA478L); /* 1 */ FF(d, a, b, c, in[1], S12, 0xE8C7B756L); /* 2 */ FF(c, d, a, b, in[2], S13, 0x242070DBL); /* 3 */ FF(b, c, d, a, in[3], S14, 0xC1BDCEEEL); /* 4 */ FF(a, b, c, d, in[4], S11, 0xF57C0FAFL); /* 5 */ FF(d, a, b, c, in[5], S12, 0x4787C62AL); /* 6 */ FF(c, d, a, b, in[6], S13, 0xA8304613L); /* 7 */ FF(b, c, d, a, in[7], S14, 0xFD469501L); /* 8 */ FF(a, b, c, d, in[8], S11, 0x698098D8L); /* 9 */ FF(d, a, b, c, in[9], S12, 0x8B44F7AFL); /* 10 */ FF(c, d, a, b, in[10], S13, 0xFFFF5BB1L); /* 11 */ FF(b, c, d, a, in[11], S14, 0x895CD7BEL); /* 12 */ FF(a, b, c, d, in[12], S11, 0x6B901122L); /* 13 */ FF(d, a, b, c, in[13], S12, 0xFD987193L); /* 14 */ FF(c, d, a, b, in[14], S13, 0xA679438EL); /* 15 */ FF(b, c, d, a, in[15], S14, 0x49B40821L); /* 16 */ /* Round 2 */ #define S21 5 #define S22 9 #define S23 14 #define S24 20 GG(a, b, c, d, in[1], S21, 0xF61E2562L); /* 17 */ GG(d, a, b, c, in[6], S22, 0xC040B340L); /* 18 */ GG(c, d, a, b, in[11], S23, 0x265E5A51L); /* 19 */ GG(b, c, d, a, in[0], S24, 0xE9B6C7AAL); /* 20 */ GG(a, b, c, d, in[5], S21, 0xD62F105DL); /* 21 */ GG(d, a, b, c, in[10], S22, 0x02441453L); /* 22 */ GG(c, d, a, b, in[15], S23, 0xD8A1E681L); /* 23 */ GG(b, c, d, a, in[4], S24, 0xE7D3FBC8L); /* 24 */ GG(a, b, c, d, in[9], S21, 0x21E1CDE6L); /* 25 */ GG(d, a, b, c, in[14], S22, 0xC33707D6L); /* 26 */ GG(c, d, a, b, in[3], S23, 0xF4D50D87L); /* 27 */ GG(b, c, d, a, in[8], S24, 0x455A14EDL); /* 28 */ GG(a, b, c, d, in[13], S21, 0xA9E3E905L); /* 29 */ GG(d, a, b, c, in[2], S22, 0xFCEFA3F8L); /* 30 */ GG(c, d, a, b, in[7], S23, 0x676F02D9L); /* 31 */ GG(b, c, d, a, in[12], S24, 0x8D2A4C8AL); /* 32 */ /* Round 3 */ #define S31 4 #define S32 11 #define S33 16 #define S34 23 HH(a, b, c, d, in[5], S31, 0xFFFA3942L); /* 33 */ HH(d, a, b, c, in[8], S32, 0x8771F681L); /* 34 */ HH(c, d, a, b, in[11], S33, 0x6D9D6122L); /* 35 */ HH(b, c, d, a, in[14], S34, 0xFDE5380CL); /* 36 */ HH(a, b, c, d, in[1], S31, 0xA4BEEA44L); /* 37 */ HH(d, a, b, c, in[4], S32, 0x4BDECFA9L); /* 38 */ HH(c, d, a, b, in[7], S33, 0xF6BB4B60L); /* 39 */ HH(b, c, d, a, in[10], S34, 0xBEBFBC70L); /* 40 */ HH(a, b, c, d, in[13], S31, 0x289B7EC6L); /* 41 */ HH(d, a, b, c, in[0], S32, 0xEAA127FAL); /* 42 */ HH(c, d, a, b, in[3], S33, 0xD4EF3085L); /* 43 */ HH(b, c, d, a, in[6], S34, 0x04881D05L); /* 44 */ HH(a, b, c, d, in[9], S31, 0xD9D4D039L); /* 45 */ HH(d, a, b, c, in[12], S32, 0xE6DB99E5L); /* 46 */ HH(c, d, a, b, in[15], S33, 0x1FA27CF8L); /* 47 */ HH(b, c, d, a, in[2], S34, 0xC4AC5665L); /* 48 */ /* Round 4 */ #define S41 6 #define S42 10 #define S43 15 #define S44 21 II(a, b, c, d, in[0], S41, 0xF4292244L); /* 49 */ II(d, a, b, c, in[7], S42, 0x432AFF97L); /* 50 */ II(c, d, a, b, in[14], S43, 0xAB9423A7L); /* 51 */ II(b, c, d, a, in[5], S44, 0xFC93A039L); /* 52 */ II(a, b, c, d, in[12], S41, 0x655B59C3L); /* 53 */ II(d, a, b, c, in[3], S42, 0x8F0CCC92L); /* 54 */ II(c, d, a, b, in[10], S43, 0xFFEFF47DL); /* 55 */ II(b, c, d, a, in[1], S44, 0x85845DD1L); /* 56 */ II(a, b, c, d, in[8], S41, 0x6FA87E4FL); /* 57 */ II(d, a, b, c, in[15], S42, 0xFE2CE6E0L); /* 58 */ II(c, d, a, b, in[6], S43, 0xA3014314L); /* 59 */ II(b, c, d, a, in[13], S44, 0x4E0811A1L); /* 60 */ II(a, b, c, d, in[4], S41, 0xF7537E82L); /* 61 */ II(d, a, b, c, in[11], S42, 0xBD3AF235L); /* 62 */ II(c, d, a, b, in[2], S43, 0x2AD7D2BBL); /* 63 */ II(b, c, d, a, in[9], S44, 0xEB86D391L); /* 64 */ buf[0] += a; buf[1] += b; buf[2] += c; buf[3] += d; } gchar *gtksfv_MD5_Digest(guchar digest[16]) { GString *string; g_return_val_if_fail(digest != NULL, NULL); string = g_string_new(NULL); { gint i; for(i = 0; i < 16; i++) g_string_append_printf(string, "%02x", digest[i]); } return(g_string_free(string, FALSE)); } gchar *gtksfv_MD5_File(const gchar *file) { guchar digest[16]; FILE *fp; MD5_CTX ctx; g_return_val_if_fail(file != NULL && file[0] != '\0', NULL); if(strcmp(file, "-") == 0) { fp = stdin; } else { if((fp = fopen(file, "rb")) == NULL) { perror(file); return(NULL); } } rsa_MD5_Init(&ctx); { gint bytes; gchar buf[4096]; while((bytes = fread(buf, 1, sizeof(buf), fp)) != 0) { rsa_MD5_Update(&ctx, buf, bytes); if(bytes == 0 && ferror(fp)) return(NULL); if(feof(fp)) break; } } rsa_MD5_Final(digest, &ctx); if(fp != stdin) fclose(fp); return(gtksfv_MD5_Digest(digest)); } /* * --- Changelog -------------------------------------------------------------- * * 0.01 - 7/21/2003: * initial release. * * 0.02 - 7/22/2003: * changes to sfv parser, allowing spaces in filenames. ugh :I * changes for windows when looking for the file(s) contained in the sfv. * change to the directory where the sfv is located. * change to the directory where the files for the sfv are being selected. * check sfv item if it exists, not only if it's a file. * add file size, modification time and name comment to new sfv. * memory related cleanups. * * 0.03 - 7/22/2003: * cleanups to sfv saving (use GSLists, don't save empty). * added Options menu -- toggle to remember last used directory. * make pdsfv_GetFileCRC() return NULL on fopen() error. * test for NULL values when adding items to the list. * changed instances instances of G_PERROR() to PERROR() (stdarg). * don't save 'MISSING' or 'BAD' entries from previously created sfvs. * removed windows stuff. * * 0.04 - 7/22/2003: * include unsigned int crc value in GtkListStore, less conversion. * reorganization and other cleanups. * * 0.05 - 7/29/2003: * added md5sum support; all checksum files are now assumed to be md5 * unless the file name contains .sfv both common md5 file formats * are supported. * crc values are stored and processed as strings again. * added Usage comment at the top and many comments. * other cleanups. * * 0.06 - 10/1/2003: * added prompt on unsaved sfvs (configurable). * always save the configuration file. * other cleanups. * * 0.07 - 11/12/2003: * minor cleanups. * * --- Changelog -------------------------------------------------------------- */