+#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"
extern GPrivate *glk_data_key;
-/* Internal function: create a fileref using the given parameters. */
+/* Internal function: create a fileref using the given parameters. If @basename
+is NULL, compute a basename from @filename. */
frefid_t
-fileref_new(gchar *filename, glui32 rock, glui32 usage, glui32 orig_filemode)
+fileref_new(char *filename, char *basename, glui32 rock, glui32 usage, glui32 orig_filemode)
{
g_return_val_if_fail(filename != NULL, NULL);
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;
fref->disprock.ptr = NULL;
}
- if(fref->filename)
- g_free(fref->filename);
+ g_free(fref->filename);
+ g_free(fref->basename);
fref->magic = MAGIC_FREE;
g_free(fref);
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;
}
* <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>
*
* 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.
*/
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);
return NULL;
}
gchar *filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(chooser) );
- frefid_t f = fileref_new(filename, rock, usage, fmode);
+ frefid_t f = fileref_new(filename, NULL, rock, usage, fmode);
g_free(filename);
gtk_widget_destroy(chooser);
* 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 — 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.
*/
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'. */
- g_strdelimit(name, "/", '_');
+ 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 */
/* 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);
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;
}
* 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
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);
}
/**
* @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."));
+ }
+
}
/**