X-Git-Url: https://git.stderr.nl/gitweb?a=blobdiff_plain;f=libchimara%2Ffileref.c;h=716586c4a23c261e092a47f198d4ec6423669a8b;hb=1e0dc5378f314f555e3b923c6d95f5017abd528b;hp=08ae5843d2318156619ee1ffdf92d17342ae2370;hpb=cfdddc22cc7aa7fcfaebb74102de03de8f4ad27a;p=projects%2Fchimara%2Fchimara.git diff --git a/libchimara/fileref.c b/libchimara/fileref.c index 08ae584..716586c 100644 --- a/libchimara/fileref.c +++ b/libchimara/fileref.c @@ -1,13 +1,66 @@ +#include #include #include #include #include #include +#include #include "fileref.h" #include "magic.h" #include "chimara-glk-private.h" +#include "gi_dispa.h" -extern ChimaraGlkPrivate *glk_data; +extern GPrivate glk_data_key; + +/* Internal function: create a fileref using the given parameters. If @basename +is NULL, compute a basename from @filename. */ +frefid_t +fileref_new(char *filename, char *basename, glui32 rock, glui32 usage, glui32 orig_filemode) +{ + g_return_val_if_fail(filename != NULL, NULL); + + ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key); + + frefid_t f = g_new0(struct glk_fileref_struct, 1); + f->magic = MAGIC_FILEREF; + f->rock = rock; + if(glk_data->register_obj) + f->disprock = (*glk_data->register_obj)(f, gidisp_Class_Fileref); + + f->filename = g_strdup(filename); + if(basename) + f->basename = g_strdup(basename); + else + f->basename = g_path_get_basename(filename); + f->usage = usage; + f->orig_filemode = orig_filemode; + + /* Add it to the global fileref list */ + glk_data->fileref_list = g_list_prepend(glk_data->fileref_list, f); + f->fileref_list = glk_data->fileref_list; + + return f; +} + +static void +fileref_close_common(frefid_t fref) +{ + ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key); + + glk_data->fileref_list = g_list_delete_link(glk_data->fileref_list, fref->fileref_list); + + if(glk_data->unregister_obj) + { + (*glk_data->unregister_obj)(fref, gidisp_Class_Fileref, fref->disprock); + fref->disprock.ptr = NULL; + } + + g_free(fref->filename); + g_free(fref->basename); + + fref->magic = MAGIC_FREE; + g_free(fref); +} /** * glk_fileref_iterate: @@ -24,7 +77,8 @@ frefid_t glk_fileref_iterate(frefid_t fref, glui32 *rockptr) { VALID_FILEREF_OR_NULL(fref, return NULL); - + + ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key); GList *retnode; if(fref == NULL) @@ -56,26 +110,6 @@ glk_fileref_get_rock(frefid_t fref) return fref->rock; } -/* Internal function: create a fileref using the given parameters. */ -frefid_t -fileref_new(gchar *filename, glui32 rock, glui32 usage, glui32 orig_filemode) -{ - g_return_val_if_fail(filename != NULL, NULL); - - frefid_t f = g_new0(struct glk_fileref_struct, 1); - f->magic = MAGIC_FILEREF; - f->rock = rock; - f->filename = g_strdup(filename); - f->usage = usage; - f->orig_filemode = orig_filemode; - - /* Add it to the global fileref list */ - glk_data->fileref_list = g_list_prepend(glk_data->fileref_list, f); - f->fileref_list = glk_data->fileref_list; - - return f; -} - /** * glk_fileref_create_temp: * @usage: Bitfield with one or more of the fileusage_ constants. @@ -118,7 +152,8 @@ glk_fileref_create_temp(glui32 usage, glui32 rock) return NULL; } - frefid_t f = fileref_new(filename, rock, usage, filemode_Write); + /* Pass a basename of "" to ensure that this file can't be repurposed */ + frefid_t f = fileref_new(filename, "", rock, usage, filemode_Write); g_free(filename); return f; } @@ -137,30 +172,31 @@ glk_fileref_create_temp(glui32 usage, glui32 rock) * Chimara * * Chimara uses a GtkFileChooserDialog. + * linkend="GtkFileChooserDialog">GtkFileChooserDialog. The default + * starting location for the dialog may be set with glkunix_set_base_file(). * * * @fmode must be one of these values: * * - * #filemode_Read + * %filemode_Read * The file must already exist; and the player will be asked * to select from existing files which match the usage. * * - * #filemode_Write + * %filemode_Write * The file should not exist; if the player selects an * existing file, he will be warned that it will be replaced. * * * - * #filemode_ReadWrite + * %filemode_ReadWrite * The file may or may not exist; if it already exists, the * player will be warned that it will be modified. * * - * #filemode_WriteAppend - * Same behavior as #filemode_ReadWrite. + * %filemode_WriteAppend + * Same behavior as %filemode_ReadWrite. * * * @@ -168,12 +204,17 @@ glk_fileref_create_temp(glui32 usage, glui32 rock) * open the file. * * - * It is possible that the prompt or file tool will have a - * cancel option. If the player chooses this, - * glk_fileref_create_by_prompt() will return %NULL. This is a major reason - * why you should make sure the return value is valid before you use it. + * It is likely that the prompt or file tool will have a cancel + * option. If the player chooses this, glk_fileref_create_by_prompt() will + * return %NULL. This is a major reason why you should make sure the return + * value is valid before you use it. * * + * The recommended file suffixes for files are .glkdata for + * %fileusage_Data, .glksave for %fileusage_SavedGame, + * .txt for %fileusage_Transcript and + * %fileusage_InputRecord. + * * Returns: A new fileref, or #NULL if the fileref creation failed or the * dialog was canceled. */ @@ -184,6 +225,8 @@ glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock) for each usage */ GtkWidget *chooser; + ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key); + gdk_threads_enter(); switch(fmode) @@ -194,8 +237,7 @@ glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock) GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL); - gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), - GTK_FILE_CHOOSER_ACTION_OPEN); + gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_OPEN); break; case filemode_Write: chooser = gtk_file_chooser_dialog_new("Select a file to save to", NULL, @@ -203,10 +245,8 @@ glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock) GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL); - gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), - GTK_FILE_CHOOSER_ACTION_SAVE); - gtk_file_chooser_set_do_overwrite_confirmation( - GTK_FILE_CHOOSER(chooser), TRUE); + gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_SAVE); + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(chooser), TRUE); break; case filemode_ReadWrite: case filemode_WriteAppend: @@ -215,8 +255,7 @@ glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock) GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL); - gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), - GTK_FILE_CHOOSER_ACTION_SAVE); + gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_SAVE); break; default: ILLEGAL_PARAM("Unknown file mode: %u", fmode); @@ -224,15 +263,59 @@ glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock) return NULL; } + /* Set up a file filter with suggested extensions */ + GtkFileFilter *filter = gtk_file_filter_new(); + switch(usage & fileusage_TypeMask) + { + case fileusage_Data: + gtk_file_filter_set_name(filter, _("Data files (*.glkdata)")); + gtk_file_filter_add_pattern(filter, "*.glkdata"); + break; + case fileusage_SavedGame: + gtk_file_filter_set_name(filter, _("Saved games (*.glksave)")); + gtk_file_filter_add_pattern(filter, "*.glksave"); + break; + case fileusage_InputRecord: + gtk_file_filter_set_name(filter, _("Text files (*.txt)")); + gtk_file_filter_add_pattern(filter, "*.txt"); + break; + case fileusage_Transcript: + gtk_file_filter_set_name(filter, _("Transcript files (*.txt)")); + gtk_file_filter_add_pattern(filter, "*.txt"); + break; + default: + ILLEGAL_PARAM("Unknown file usage: %u", usage); + gdk_threads_leave(); + return NULL; + } + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter); + + /* Add a "text mode" filter for text files */ + if((usage & fileusage_TypeMask) == fileusage_InputRecord || (usage & fileusage_TypeMask) == fileusage_Transcript) + { + filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, _("All text files")); + gtk_file_filter_add_mime_type(filter, "text/plain"); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter); + } + + /* Add another non-restricted filter */ + filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, _("All files")); + gtk_file_filter_add_pattern(filter, "*"); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter); + + if(glk_data->current_dir) + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(chooser), glk_data->current_dir); + if(gtk_dialog_run( GTK_DIALOG(chooser) ) != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(chooser); gdk_threads_leave(); return NULL; } - gchar *filename = - gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(chooser) ); - frefid_t f = fileref_new(filename, rock, usage, fmode); + gchar *filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(chooser) ); + frefid_t f = fileref_new(filename, NULL, rock, usage, fmode); g_free(filename); gtk_widget_destroy(chooser); @@ -254,28 +337,93 @@ glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock) * * Chimara * - * In Chimara, the file is created in the current working directory. + * In Chimara, the file is created in the directory last set by + * glkunix_set_base_file(), and otherwise in the current working directory. * * - * Since filenames are highly platform-specific, you should use - * glk_fileref_create_by_name() with care. It is legal to pass any string in the - * name argument. However, the library may have to mangle, transform, or - * truncate the string to make it a legal native filename. + * Earlier versions of the Glk spec specified that the library may have to + * extend, truncate, or change your name argument in order to produce a legal + * native filename. This remains true. However, since Glk was originally + * proposed, the world has largely reached consensus about what a filename looks + * like. Therefore, it is worth including some recommended library behavior + * here. Libraries that share this behavior will more easily be able to exchange + * files, which may be valuable both to authors (distributing data files for + * games) and for players (moving data between different computers or different + * applications). + * + * The library should take the given filename argument, and delete any + * characters illegal for a filename. This will include all of the following + * characters (and more, if the OS requires it): slash, backslash, angle + * brackets (less-than and greater-than), colon, double-quote, pipe (vertical + * bar), question-mark, asterisk. The library should also truncate the argument + * at the first period (delete the first period and any following characters). + * If the result is the empty string, change it to the string + * "null". + * + * It should then append an appropriate suffix, depending on the usage: + * .glkdata for %fileusage_Data, + * .glksave for %fileusage_SavedGame, + * .txt for %fileusage_Transcript and + * %fileusage_InputRecord. + * + * The above behavior is not a requirement of the Glk spec. Older + * implementations can continue doing what they do. Some programs (e.g. + * web-based interpreters) may not have access to a traditional filesystem at + * all, and to them these recommendations will be meaningless. + * + * On the other side of the coin, the game file should not press these + * limitations. Best practice is for the game to pass a filename containing only + * letters and digits, beginning with a letter, and not mixing upper and lower + * case. Avoid overly-long filenames. * * - * For example, if you create two filerefs with the names File - * and FILE, they may wind up pointing to the same file; the - * platform may not support case distinctions in file names. Another example: - * on a platform where file type is specified by filename suffix, the library - * will add an appropriate suffix based on the usage; any suffix in the string - * will be overwritten or added to. For that matter, remember that the period - * is not a legal character in Acorn filenames... + * The earlier Glk spec gave more stringent recommendations: No more + * than 8 characters, consisting entirely of upper-case letters and numbers, + * starting with a letter. The DOS era is safely contained, if not + * over, so this has been relaxed. The I7 manual recommends 23 + * characters or fewer. * * - * The most conservative approach is to pass a string of no more than 8 - * characters, consisting entirely of upper-case letters and numbers, starting - * with a letter. You can then be reasonably sure that the resulting filename - * will display all the characters you specify — in some form. + * + * To address other complications: + * + * + * Some filesystems are case-insensitive. If you create two filerefs with + * the names File and FILE, they + * may wind up pointing to the same file, or they may not. Avoid doing + * this. + * + * + * Some programs will look for all files in the same directory as the + * program itself (or, for interpreted games, in the same directory as the + * game file). Others may keep files in a data-specific directory + * appropriate for the user (e.g., ~/Library on MacOS). + * + * + * If a game interpreter uses a data-specific directory, there is a + * question of whether to use a common location, or divide it into + * game-specific subdirectories. (Or to put it another way: should the + * namespace of named files be per-game or app-wide?) Since data files may + * be exchanged between games, they should be given an app-wide namespace. + * In contrast, saved games should be per-game, as they can never be + * exchanged. Transcripts and input records can go either way. + * + * + * When updating an older library to follow these recommendations, + * consider backwards compatibility for games already installed. When + * opening an existing file (that is, not in a write-only mode) it may be + * worth looking under the older name (suffix) if the newer one does not + * already exist. + * + * + * Game-save files are already stored with a variety of file suffixes, + * since that usage goes back to the oldest IF interpreters, long + * predating Glk. It is reasonable to treat them in some special way, + * while hewing closer to these recommendations for data files. + * + * + * * * Returns: A new fileref, or %NULL if the fileref creation failed. */ @@ -284,27 +432,74 @@ glk_fileref_create_by_name(glui32 usage, char *name, glui32 rock) { g_return_val_if_fail(name != NULL && strlen(name) > 0, NULL); + ChimaraGlkPrivate *glk_data = g_private_get(&glk_data_key); + + /* Do any string-munging here to remove illegal Latin-1 characters from + filename. On ext3, the only illegal characters are '/' and '\0', but the Glk + spec calls for removing any other tricky characters. */ + char *buf = g_malloc(strlen(name)); + char *ptr, *filename, *extension; + int len; + for(ptr = name, len = 0; *ptr && *ptr != '.'; ptr++) + { + switch(*ptr) + { + case '"': case '\\': case '/': case '>': case '<': + case ':': case '|': case '?': case '*': + break; + default: + buf[len++] = *ptr; + } + } + buf[len] = '\0'; + + /* If there is nothing left, make the name "null" */ + if(len == 0) { + strcpy(buf, "null"); + len = strlen(buf); + } + + switch(usage & fileusage_TypeMask) + { + case fileusage_Data: + extension = ".glkdata"; + break; + case fileusage_SavedGame: + extension = ".glksave"; + break; + case fileusage_InputRecord: + case fileusage_Transcript: + extension = ".txt"; + break; + default: + ILLEGAL_PARAM("Unknown file usage: %u", usage); + return NULL; + } + filename = g_strconcat(buf, extension, NULL); + /* Find out what encoding filenames are in */ const gchar **charsets; /* Do not free */ g_get_filename_charsets(&charsets); /* Convert name to that encoding */ GError *error = NULL; - gchar *osname = g_convert(name, -1, charsets[0], "ISO-8859-1", NULL, NULL, - &error); + char *osname = g_convert(filename, -1, charsets[0], "ISO-8859-1", NULL, NULL, &error); if(osname == NULL) { WARNING_S("Error during latin1->filename conversion", error->message); return NULL; } - - /* Do any string-munging here to remove illegal characters from filename. - On ext3, the only illegal characters are '/' and '\0'. TODO: Should this - function be allowed to reference files in other directories, or should we - disallow '/'? */ - - frefid_t f = fileref_new(osname, rock, usage, filemode_ReadWrite); + + gchar *path; + if(glk_data->current_dir) + path = g_build_filename(glk_data->current_dir, osname, NULL); + else + path = g_strdup(osname); g_free(osname); + + frefid_t f = fileref_new(path, buf, rock, usage, filemode_ReadWrite); + g_free(path); + g_free(buf); return f; } @@ -318,20 +513,20 @@ glk_fileref_create_by_name(glui32 usage, char *name, glui32 rock) * original fileref is not modified.) * * The use of this function can be tricky. If you change the type of the fileref - * (#fileusage_Data, #fileusage_SavedGame, etc), the new reference may or may + * (%fileusage_Data, %fileusage_SavedGame, etc), the new reference may or may * not point to the same actual disk file. * * - * This generally depends on whether the platform uses suffixes to indicate - * file type. + * Most platforms use suffixes to indicate file type, so it typically will + * not. See the earlier comments about recommended file suffixes. * * * If you do this, and open both file references for writing, the results are * unpredictable. It is safest to change the type of a fileref only if it refers * to a nonexistent file. * - * If you change the mode of a fileref (#fileusage_TextMode, - * #fileusage_BinaryMode), but leave the rest of the type unchanged, the new + * If you change the mode of a fileref (%fileusage_TextMode, + * %fileusage_BinaryMode), but leave the rest of the type unchanged, the new * fileref will definitely point to the same disk file as the old one. * * Obviously, if you write to a file in text mode and then read from it in @@ -343,7 +538,7 @@ frefid_t glk_fileref_create_from_fileref(glui32 usage, frefid_t fref, glui32 rock) { VALID_FILEREF(fref, return NULL); - return fileref_new(fref->filename, rock, usage, fref->orig_filemode); + return fileref_new(fref->filename, fref->basename, rock, usage, fref->orig_filemode); } /** @@ -362,13 +557,7 @@ void glk_fileref_destroy(frefid_t fref) { VALID_FILEREF(fref, return); - - glk_data->fileref_list = g_list_delete_link(glk_data->fileref_list, fref->fileref_list); - if(fref->filename) - g_free(fref->filename); - - fref->magic = MAGIC_FREE; - g_free(fref); + fileref_close_common(fref); } /** @@ -376,14 +565,23 @@ glk_fileref_destroy(frefid_t fref) * @fref: A refrence to the file to delete. * * Deletes the file referred to by @fref. It does not destroy @fref itself. + * + * You should only call this with a fileref that refers to an existing file. */ void glk_fileref_delete_file(frefid_t fref) { VALID_FILEREF(fref, return); if( glk_fileref_does_file_exist(fref) ) + { if(g_unlink(fref->filename) == -1) IO_WARNING( "Error deleting file", fref->filename, g_strerror(errno) ); + } + else + { + ILLEGAL(_("Tried to delete a fileref that does not refer to an existing file.")); + } + } /**