Implement recommended file suffixes in dialog
[projects/chimara/chimara.git] / libchimara / fileref.c
1 #include <config.h>
2 #include <errno.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <gtk/gtk.h>
6 #include <glib/gstdio.h>
7 #include <glib/gi18n-lib.h>
8 #include "fileref.h"
9 #include "magic.h"
10 #include "chimara-glk-private.h"
11 #include "gi_dispa.h"
12
13 extern GPrivate *glk_data_key;
14
15 /* Internal function: create a fileref using the given parameters. */
16 frefid_t
17 fileref_new(gchar *filename, glui32 rock, glui32 usage, glui32 orig_filemode)
18 {
19         g_return_val_if_fail(filename != NULL, NULL);
20
21         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
22         
23         frefid_t f = g_new0(struct glk_fileref_struct, 1);
24         f->magic = MAGIC_FILEREF;
25         f->rock = rock;
26         if(glk_data->register_obj)
27                 f->disprock = (*glk_data->register_obj)(f, gidisp_Class_Fileref);
28         
29         f->filename = g_strdup(filename);
30         f->usage = usage;
31         f->orig_filemode = orig_filemode;
32         
33         /* Add it to the global fileref list */
34         glk_data->fileref_list = g_list_prepend(glk_data->fileref_list, f);
35         f->fileref_list = glk_data->fileref_list;
36         
37         return f;
38 }
39
40 static void
41 fileref_close_common(frefid_t fref)
42 {
43         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
44         
45         glk_data->fileref_list = g_list_delete_link(glk_data->fileref_list, fref->fileref_list);
46
47         if(glk_data->unregister_obj)
48         {
49                 (*glk_data->unregister_obj)(fref, gidisp_Class_Fileref, fref->disprock);
50                 fref->disprock.ptr = NULL;
51         }
52         
53         if(fref->filename)
54                 g_free(fref->filename);
55         
56         fref->magic = MAGIC_FREE;
57         g_free(fref);
58 }
59
60 /**
61  * glk_fileref_iterate:
62  * @fref: A file reference, or %NULL.
63  * @rockptr: Return location for the next fileref's rock, or %NULL.
64  *
65  * Iterates through all the existing filerefs. See <link
66  * linkend="chimara-Iterating-Through-Opaque-Objects">Iterating Through Opaque
67  * Objects</link>.
68  *
69  * Returns: the next file reference, or %NULL if there are no more.
70  */
71 frefid_t
72 glk_fileref_iterate(frefid_t fref, glui32 *rockptr)
73 {
74         VALID_FILEREF_OR_NULL(fref, return NULL);
75
76         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
77         GList *retnode;
78         
79         if(fref == NULL)
80                 retnode = glk_data->fileref_list;
81         else
82                 retnode = fref->fileref_list->next;
83         frefid_t retval = retnode? (frefid_t)retnode->data : NULL;
84                 
85         /* Store the fileref's rock in rockptr */
86         if(retval && rockptr)
87                 *rockptr = glk_fileref_get_rock(retval);
88                 
89         return retval;
90 }
91
92 /**
93  * glk_fileref_get_rock:
94  * @fref: A file reference.
95  * 
96  * Retrieves the file reference @fref's rock value. See <link 
97  * linkend="chimara-Rocks">Rocks</link>.
98  *
99  * Returns: A rock value.
100  */
101 glui32
102 glk_fileref_get_rock(frefid_t fref)
103 {
104         VALID_FILEREF(fref, return 0);
105         return fref->rock;
106 }
107
108 /**
109  * glk_fileref_create_temp:
110  * @usage: Bitfield with one or more of the <code>fileusage_</code> constants.
111  * @rock: The new fileref's rock value.
112  *
113  * Creates a reference to a temporary file. It is always a new file (one which
114  * does not yet exist). The file (once created) will be somewhere out of the
115  * player's way.
116  *
117  * <note><para>
118  *   This is why no name is specified; the player will never need to know it.
119  * </para></note>
120  *
121  * A temporary file should never be used for long-term storage. It may be
122  * deleted automatically when the program exits, or at some later time, say
123  * when the machine is turned off or rebooted. You do not have to worry about
124  * deleting it yourself.
125  *
126  * Returns: A new fileref, or #NULL if the fileref creation failed.
127  */ 
128 frefid_t
129 glk_fileref_create_temp(glui32 usage, glui32 rock)
130 {
131         /* Get a temp file */
132         GError *error = NULL;
133         gchar *filename = NULL;
134         gint handle = g_file_open_tmp("glkXXXXXX", &filename, &error);
135         if(handle == -1)
136         {
137                 WARNING_S("Error creating temporary file", error->message);
138                 if(filename)
139                         g_free(filename);
140                 return NULL;
141         }
142         if(close(handle) == -1) /* There is no g_close() */
143         {
144                 IO_WARNING( "Error closing temporary file", filename, g_strerror(errno) );
145                 if(filename)
146                         g_free(filename);
147                 return NULL;
148         }
149         
150         frefid_t f = fileref_new(filename, rock, usage, filemode_Write);
151         g_free(filename);
152         return f;
153 }
154
155 /**
156  * glk_fileref_create_by_prompt:
157  * @usage: Bitfield with one or more of the <code>fileusage_</code> constants.
158  * @fmode: File mode, contolling the dialog's behavior.
159  * @rock: The new fileref's rock value.
160  *
161  * Creates a reference to a file by asking the player to locate it. The library
162  * may simply prompt the player to type a name, or may use a platform-native
163  * file navigation tool. (The prompt, if any, is inferred from the usage
164  * argument.)
165  *
166  * <note><title>Chimara</title>
167  * <para>
168  * Chimara uses a <link 
169  * linkend="GtkFileChooserDialog">GtkFileChooserDialog</link>. The default
170  * starting location for the dialog may be set with glkunix_set_base_file().
171  * </para></note>
172  *
173  * @fmode must be one of these values:
174  * <variablelist>
175  * <varlistentry>
176  *   <term>%filemode_Read</term>
177  *   <listitem><para>The file must already exist; and the player will be asked
178  *   to select from existing files which match the usage.</para></listitem>
179  * </varlistentry>
180  * <varlistentry>
181  *   <term>%filemode_Write</term>
182  *   <listitem><para>The file should not exist; if the player selects an
183  *   existing file, he will be warned that it will be replaced.
184  *   </para></listitem>
185  * </varlistentry>
186  * <varlistentry>
187  *   <term>%filemode_ReadWrite</term>
188  *   <listitem><para>The file may or may not exist; if it already exists, the
189  *   player will be warned that it will be modified.</para></listitem>
190  * </varlistentry>
191  * <varlistentry>
192  *   <term>%filemode_WriteAppend</term>
193  *   <listitem><para>Same behavior as %filemode_ReadWrite.</para></listitem>
194  * </varlistentry>
195  * </variablelist>
196  *
197  * The @fmode argument should generally match the @fmode which will be used to
198  * open the file.
199  *
200  * <note><para>
201  *   It is likely that the prompt or file tool will have a <quote>cancel</quote>
202  *   option. If the player chooses this, glk_fileref_create_by_prompt() will
203  *   return %NULL. This is a major reason why you should make sure the return
204  *   value is valid before you use it.
205  * </para></note>
206  *
207  * The recommended file suffixes for files are <filename>.glkdata</filename> for
208  * %fileusage_Data, <filename>.glksave</filename> for %fileusage_SavedGame,
209  * <filename>.txt</filename> for %fileusage_Transcript and
210  * %fileusage_InputRecord.
211  *
212  * Returns: A new fileref, or #NULL if the fileref creation failed or the
213  * dialog was canceled.
214  */
215 frefid_t
216 glk_fileref_create_by_prompt(glui32 usage, glui32 fmode, glui32 rock)
217 {
218         /* TODO: Remember current working directory and last used filename
219         for each usage */
220         GtkWidget *chooser;
221
222         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
223         
224         gdk_threads_enter();
225
226         switch(fmode)
227         {
228                 case filemode_Read:
229                         chooser = gtk_file_chooser_dialog_new("Select a file to open", NULL,
230                                 GTK_FILE_CHOOSER_ACTION_OPEN,
231                                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
232                                 GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
233                                 NULL);
234                         gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_OPEN);
235                         break;
236                 case filemode_Write:
237                         chooser = gtk_file_chooser_dialog_new("Select a file to save to", NULL,
238                                 GTK_FILE_CHOOSER_ACTION_SAVE,
239                                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
240                                 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
241                                 NULL);
242                         gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_SAVE);
243
244                         /* COMPAT: */
245 #if GTK_CHECK_VERSION(2,8,0)
246                         gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(chooser), TRUE);
247 #endif
248                         break;
249                 case filemode_ReadWrite:
250                 case filemode_WriteAppend:
251                         chooser = gtk_file_chooser_dialog_new("Select a file to save to", NULL,
252                                 GTK_FILE_CHOOSER_ACTION_SAVE,
253                                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
254                                 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
255                                 NULL);
256                         gtk_file_chooser_set_action(GTK_FILE_CHOOSER(chooser), GTK_FILE_CHOOSER_ACTION_SAVE);
257                         break;
258                 default:
259                         ILLEGAL_PARAM("Unknown file mode: %u", fmode);
260                         gdk_threads_leave();
261                         return NULL;
262         }
263         
264         /* Set up a file filter with suggested extensions */
265         GtkFileFilter *filter = gtk_file_filter_new();
266         switch(usage & fileusage_TypeMask)
267         {
268                 case fileusage_Data:
269                         gtk_file_filter_set_name(filter, _("Data files (*.glkdata)"));
270                         gtk_file_filter_add_pattern(filter, "*.glkdata");
271                         break;
272                 case fileusage_SavedGame:
273                         gtk_file_filter_set_name(filter, _("Saved games (*.glksave)"));
274                         gtk_file_filter_add_pattern(filter, "*.glksave");
275                         break;
276                 case fileusage_InputRecord:
277                         gtk_file_filter_set_name(filter, _("Text files (*.txt)"));
278                         gtk_file_filter_add_pattern(filter, "*.txt");
279                         break;
280                 case fileusage_Transcript:
281                         gtk_file_filter_set_name(filter, _("Transcript files (*.txt)"));
282                         gtk_file_filter_add_pattern(filter, "*.txt");
283                         break;
284                 default:
285                         ILLEGAL_PARAM("Unknown file usage: %u", usage);
286                         gdk_threads_leave();
287                         return NULL;
288         }
289         gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);
290
291         /* Add a "text mode" filter for text files */
292         if((usage & fileusage_TypeMask) == fileusage_InputRecord || (usage & fileusage_TypeMask) == fileusage_Transcript)
293         {
294                 filter = gtk_file_filter_new();
295                 gtk_file_filter_set_name(filter, _("All text files"));
296                 gtk_file_filter_add_mime_type(filter, "text/plain");
297                 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);
298         }
299
300         /* Add another non-restricted filter */
301         filter = gtk_file_filter_new();
302         gtk_file_filter_set_name(filter, _("All files"));
303         gtk_file_filter_add_pattern(filter, "*");
304         gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);
305
306         if(glk_data->current_dir)
307                 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(chooser), glk_data->current_dir);
308         
309         if(gtk_dialog_run( GTK_DIALOG(chooser) ) != GTK_RESPONSE_ACCEPT)
310         {
311                 gtk_widget_destroy(chooser);
312                 gdk_threads_leave();
313                 return NULL;
314         }
315         gchar *filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(chooser) );
316         frefid_t f = fileref_new(filename, rock, usage, fmode);
317         g_free(filename);
318         gtk_widget_destroy(chooser);
319
320         gdk_threads_leave();
321         return f;
322 }
323
324 /**
325  * glk_fileref_create_by_name:
326  * @usage: Bitfield with one or more of the <code>fileusage_</code> constants.
327  * @name: A filename.
328  * @rock: The new fileref's rock value.
329  *
330  * This creates a reference to a file with a specific name. The file will be
331  * in a fixed location relevant to your program, and visible to the player.
332  *
333  * <note><para>
334  *   This usually means <quote>in the same directory as your program.</quote>
335  * </para></note>
336  * <note><title>Chimara</title>
337  * <para>
338  * In Chimara, the file is created in the directory last set by 
339  * glkunix_set_base_file(), and otherwise in the current working directory.
340  * </para></note>
341  *
342  * Earlier versions of the Glk spec specified that the library may have to
343  * extend, truncate, or change your name argument in order to produce a legal
344  * native filename. This remains true. However, since Glk was originally
345  * proposed, the world has largely reached consensus about what a filename looks
346  * like. Therefore, it is worth including some recommended library behavior
347  * here. Libraries that share this behavior will more easily be able to exchange
348  * files, which may be valuable both to authors (distributing data files for
349  * games) and for players (moving data between different computers or different
350  * applications).
351  *
352  * The library should take the given filename argument, and delete any
353  * characters illegal for a filename. This will include all of the following
354  * characters (and more, if the OS requires it): slash, backslash, angle
355  * brackets (less-than and greater-than), colon, double-quote, pipe (vertical
356  * bar), question-mark, asterisk. The library should also truncate the argument
357  * at the first period (delete the first period and any following characters).
358  * If the result is the empty string, change it to the string
359  * <code>"null"</code>.
360  *
361  * It should then append an appropriate suffix, depending on the usage:
362  * <filename>.glkdata</filename> for %fileusage_Data,
363  * <filename>.glksave</filename> for %fileusage_SavedGame,
364  * <filename>.txt</filename> for %fileusage_Transcript and
365  * %fileusage_InputRecord.
366  *
367  * The above behavior is not a requirement of the Glk spec. Older
368  * implementations can continue doing what they do. Some programs (e.g.
369  * web-based interpreters) may not have access to a traditional filesystem at
370  * all, and to them these recommendations will be meaningless.
371  *
372  * On the other side of the coin, the game file should not press these
373  * limitations. Best practice is for the game to pass a filename containing only
374  * letters and digits, beginning with a letter, and not mixing upper and lower
375  * case. Avoid overly-long filenames.
376  *
377  * <note><para>
378  *   The earlier Glk spec gave more stringent recommendations: <quote>No more
379  *   than 8 characters, consisting entirely of upper-case letters and numbers,
380  *   starting with a letter</quote>. The DOS era is safely contained, if not
381  *   over, so this has been relaxed. The I7 manual recommends <quote>23
382  *   characters or fewer</quote>.
383  * </para></note>
384  *
385  * <note><para>
386  *   To address other complications:</para>
387  *   <itemizedlist>
388  *     <listitem><para>
389  *       Some filesystems are case-insensitive. If you create two filerefs with
390  *       the names <filename>File</filename> and <filename>FILE</filename>, they
391  *       may wind up pointing to the same file, or they may not. Avoid doing
392  *       this.
393  *     </para></listitem>
394  *     <listitem><para>
395  *       Some programs will look for all files in the same directory as the
396  *       program itself (or, for interpreted games, in the same directory as the
397  *       game file). Others may keep files in a data-specific directory
398  *       appropriate for the user (e.g., <filename
399  *       class="directory">~/Library</filename> on MacOS).
400  *     </para></listitem>
401  *     <listitem><para>
402  *       If a game interpreter uses a data-specific directory, there is a
403  *       question of whether to use a common location, or divide it into
404  *       game-specific subdirectories. (Or to put it another way: should the
405  *       namespace of named files be per-game or app-wide?) Since data files may
406  *       be exchanged between games, they should be given an app-wide namespace.
407  *       In contrast, saved games should be per-game, as they can never be
408  *       exchanged. Transcripts and input records can go either way.
409  *     </para></listitem>
410  *     <listitem><para>
411  *       When updating an older library to follow these recommendations,
412  *       consider backwards compatibility for games already installed. When
413  *       opening an existing file (that is, not in a write-only mode) it may be
414  *       worth looking under the older name (suffix) if the newer one does not
415  *       already exist.
416  *     </para></listitem>
417  *     <listitem><para>
418  *       Game-save files are already stored with a variety of file suffixes,
419  *       since that usage goes back to the oldest IF interpreters, long
420  *       predating Glk. It is reasonable to treat them in some special way,
421  *       while hewing closer to these recommendations for data files.
422  *     </para></listitem>
423  *   </itemizedlist>
424  * </note>
425  *
426  * Returns: A new fileref, or %NULL if the fileref creation failed. 
427  */
428 frefid_t
429 glk_fileref_create_by_name(glui32 usage, char *name, glui32 rock)
430 {
431         g_return_val_if_fail(name != NULL && strlen(name) > 0, NULL);
432
433         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
434         
435         /* Do any string-munging here to remove illegal Latin-1 characters from 
436         filename. On ext3, the only illegal characters are '/' and '\0'. */
437         g_strdelimit(name, "/", '_');
438         
439         /* Find out what encoding filenames are in */
440         const gchar **charsets; /* Do not free */
441         g_get_filename_charsets(&charsets);
442
443         /* Convert name to that encoding */
444         GError *error = NULL;
445         gchar *osname = g_convert(name, -1, charsets[0], "ISO-8859-1", NULL, NULL,
446                 &error);
447         if(osname == NULL)
448         {
449                 WARNING_S("Error during latin1->filename conversion", error->message);
450                 return NULL;
451         }
452         
453         gchar *path;
454         if(glk_data->current_dir)
455                 path = g_build_filename(glk_data->current_dir, osname, NULL);
456         else
457                 path = g_strdup(osname);
458         g_free(osname);
459         
460         frefid_t f = fileref_new(path, rock, usage, filemode_ReadWrite);
461         g_free(path);
462         return f;
463 }
464
465 /**
466  * glk_fileref_create_from_fileref:
467  * @usage: Bitfield with one or more of the <code>fileusage_</code> constants.
468  * @fref: Fileref to copy.
469  * @rock: The new fileref's rock value.
470  *
471  * This copies an existing file reference @fref, but changes the usage. (The
472  * original fileref is not modified.)
473  *
474  * The use of this function can be tricky. If you change the type of the fileref
475  * (%fileusage_Data, %fileusage_SavedGame, etc), the new reference may or may
476  * not point to the same actual disk file.
477  *
478  * <note><para>
479  *   Most platforms use suffixes to indicate file type, so it typically will
480  *   not. See the earlier comments about recommended file suffixes.
481  * </para></note>
482  *
483  * If you do this, and open both file references for writing, the results are
484  * unpredictable. It is safest to change the type of a fileref only if it refers
485  * to a nonexistent file.
486  *
487  * If you change the mode of a fileref (%fileusage_TextMode,
488  * %fileusage_BinaryMode), but leave the rest of the type unchanged, the new
489  * fileref will definitely point to the same disk file as the old one.
490  * 
491  * Obviously, if you write to a file in text mode and then read from it in
492  * binary mode, the results are platform-dependent. 
493  *
494  * Returns: A new fileref, or %NULL if the fileref creation failed. 
495  */
496 frefid_t
497 glk_fileref_create_from_fileref(glui32 usage, frefid_t fref, glui32 rock)
498 {
499         VALID_FILEREF(fref, return NULL);
500         return fileref_new(fref->filename, rock, usage, fref->orig_filemode);
501 }
502
503 /**
504  * glk_fileref_destroy:
505  * @fref: Fileref to destroy.
506  * 
507  * Destroys a fileref which you have created. This does <emphasis>not</emphasis>
508  * affect the disk file; it just reclaims the resources allocated by the
509  * <code>glk_fileref_create...</code> function.
510  *
511  * It is legal to destroy a fileref after opening a file with it (while the
512  * file is still open.) The fileref is only used for the opening operation,
513  * not for accessing the file stream.
514  */
515 void
516 glk_fileref_destroy(frefid_t fref)
517 {
518         VALID_FILEREF(fref, return);
519         fileref_close_common(fref);
520 }
521
522 /**
523  * glk_fileref_delete_file:
524  * @fref: A refrence to the file to delete.
525  *
526  * Deletes the file referred to by @fref. It does not destroy @fref itself.
527  *
528  * You should only call this with a fileref that refers to an existing file.
529  */
530 void
531 glk_fileref_delete_file(frefid_t fref)
532 {
533         VALID_FILEREF(fref, return);
534         if( glk_fileref_does_file_exist(fref) )
535         {
536                 if(g_unlink(fref->filename) == -1)
537                         IO_WARNING( "Error deleting file", fref->filename, g_strerror(errno) );
538         }
539         else
540         {
541                 ILLEGAL(_("Tried to delete a fileref that does not refer to an existing file."));
542         }
543
544 }
545
546 /**
547  * glk_fileref_does_file_exist:
548  * @fref: A fileref to check.
549  *
550  * Checks whether the file referred to by @fref exists.
551  *
552  * Returns: %TRUE (1) if @fref refers to an existing file, and %FALSE (0) if 
553  * not.
554  */
555 glui32
556 glk_fileref_does_file_exist(frefid_t fref)
557 {
558         VALID_FILEREF(fref, return 0);
559         if( g_file_test(fref->filename, G_FILE_TEST_EXISTS) )
560                 return 1;
561         return 0;
562 }
563