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