Use statically-allocated thread private data
[projects/chimara/chimara.git] / libchimara / fileref.c
index 516af027f6bf44157fd41b985b80458b53722fb3..716586c4a23c261e092a47f198d4ec6423669a8b 100644 (file)
@@ -1,13 +1,66 @@
+#include <config.h>
 #include <errno.h>
 #include <unistd.h>
 #include <string.h>
 #include <gtk/gtk.h>
 #include <glib/gstdio.h>
+#include <glib/gi18n-lib.h>
 #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 <code>fileusage_</code> 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,31 +172,31 @@ glk_fileref_create_temp(glui32 usage, glui32 rock)
  * <note><title>Chimara</title>
  * <para>
  * Chimara uses a <link 
- * linkend="gtk-GtkFileChooserDialog">GtkFileChooserDialog</link>. The default
+ * linkend="GtkFileChooserDialog">GtkFileChooserDialog</link>. The default
  * starting location for the dialog may be set with glkunix_set_base_file().
  * </para></note>
  *
  * @fmode must be one of these values:
  * <variablelist>
  * <varlistentry>
- *   <term>#filemode_Read</term>
+ *   <term>%filemode_Read</term>
  *   <listitem><para>The file must already exist; and the player will be asked
  *   to select from existing files which match the usage.</para></listitem>
  * </varlistentry>
  * <varlistentry>
- *   <term>#filemode_Write</term>
+ *   <term>%filemode_Write</term>
  *   <listitem><para>The file should not exist; if the player selects an
  *   existing file, he will be warned that it will be replaced.
  *   </para></listitem>
  * </varlistentry>
  * <varlistentry>
- *   <term>#filemode_ReadWrite</term>
+ *   <term>%filemode_ReadWrite</term>
  *   <listitem><para>The file may or may not exist; if it already exists, the
  *   player will be warned that it will be modified.</para></listitem>
  * </varlistentry>
  * <varlistentry>
- *   <term>#filemode_WriteAppend</term>
- *   <listitem><para>Same behavior as #filemode_ReadWrite.</para></listitem>
+ *   <term>%filemode_WriteAppend</term>
+ *   <listitem><para>Same behavior as %filemode_ReadWrite.</para></listitem>
  * </varlistentry>
  * </variablelist>
  *
@@ -169,12 +204,17 @@ glk_fileref_create_temp(glui32 usage, glui32 rock)
  * open the file.
  *
  * <note><para>
- *   It is possible that the prompt or file tool will have a 
- *   <quote>cancel</quote> 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 <quote>cancel</quote>
+ *   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.
  * </para></note>
  *
+ * The recommended file suffixes for files are <filename>.glkdata</filename> for
+ * %fileusage_Data, <filename>.glksave</filename> for %fileusage_SavedGame,
+ * <filename>.txt</filename> for %fileusage_Transcript and
+ * %fileusage_InputRecord.
+ *
  * Returns: A new fileref, or #NULL if the fileref creation failed or the
  * dialog was canceled.
  */
@@ -185,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)
@@ -195,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,
@@ -204,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:
@@ -216,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);
@@ -225,6 +263,48 @@ 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);
        
@@ -234,9 +314,8 @@ glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock)
                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);
 
@@ -262,25 +341,89 @@ glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock)
  * glkunix_set_base_file(), and otherwise in the current working directory.
  * </para></note>
  *
- * 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
+ * <code>"null"</code>.
+ *
+ * It should then append an appropriate suffix, depending on the usage:
+ * <filename>.glkdata</filename> for %fileusage_Data,
+ * <filename>.glksave</filename> for %fileusage_SavedGame,
+ * <filename>.txt</filename> 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.
  *
  * <note><para>
- *   For example, if you create two filerefs with the names <quote>File</quote>
- *   and <quote>FILE</quote>, 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: <quote>No more
+ *   than 8 characters, consisting entirely of upper-case letters and numbers,
+ *   starting with a letter</quote>. The DOS era is safely contained, if not
+ *   over, so this has been relaxed. The I7 manual recommends <quote>23
+ *   characters or fewer</quote>.
  * </para></note>
  *
- * 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 &mdash; in some form. 
+ * <note><para>
+ *   To address other complications:</para>
+ *   <itemizedlist>
+ *     <listitem><para>
+ *       Some filesystems are case-insensitive. If you create two filerefs with
+ *       the names <filename>File</filename> and <filename>FILE</filename>, they
+ *       may wind up pointing to the same file, or they may not. Avoid doing
+ *       this.
+ *     </para></listitem>
+ *     <listitem><para>
+ *       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., <filename
+ *       class="directory">~/Library</filename> on MacOS).
+ *     </para></listitem>
+ *     <listitem><para>
+ *       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.
+ *     </para></listitem>
+ *     <listitem><para>
+ *       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.
+ *     </para></listitem>
+ *     <listitem><para>
+ *       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.
+ *     </para></listitem>
+ *   </itemizedlist>
+ * </note>
  *
  * Returns: A new fileref, or %NULL if the fileref creation failed. 
  */
@@ -289,13 +432,50 @@ 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'. */
-       
-       char *ptr = name;
-       while(*ptr++)
-               if(*ptr == '/')
-                       *ptr = '_';
+       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 */
@@ -303,8 +483,7 @@ glk_fileref_create_by_name(glui32 usage, char *name, glui32 rock)
 
        /* 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);
@@ -318,8 +497,9 @@ glk_fileref_create_by_name(glui32 usage, char *name, glui32 rock)
                path = g_strdup(osname);
        g_free(osname);
        
-       frefid_t f = fileref_new(path, rock, usage, filemode_ReadWrite);
+       frefid_t f = fileref_new(path, buf, rock, usage, filemode_ReadWrite);
        g_free(path);
+       g_free(buf);
        return f;
 }
 
@@ -333,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.
  *
  * <note><para>
- *   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.
  * </para></note>
  *
  * 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
@@ -358,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);
 }
 
 /**
@@ -377,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);
 }
 
 /**
@@ -391,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."));
+       }
+
 }
 
 /**