mirror of
https://github.com/ganelson/inform.git
synced 2024-07-08 18:14:21 +03:00
419 lines
14 KiB
Plaintext
419 lines
14 KiB
Plaintext
B/iot: FileIO Template.
|
|
|
|
@Purpose: Reading and writing external files, in the Glulx virtual machine only.
|
|
|
|
@-------------------------------------------------------------------------------
|
|
|
|
@p Language.
|
|
This whole template contains material used only if the "Glulx external files"
|
|
element is part of Inform's current definition, so:
|
|
|
|
@c
|
|
#IFDEF PLUGIN_FILES;
|
|
|
|
@p Structure.
|
|
The I7 kind of value "auxiliary-file" is an |-->| array, holding a memory
|
|
structure containing information about external files. The following
|
|
constants specify memory offsets and values. Note the safety value stored
|
|
as the first word of the structure: this helps protect the routines below
|
|
from accidents. (16339, besides being prime, is a number interesting to the
|
|
author of Inform since it was the examination board identifying number of
|
|
his school, and so had to be filled in on all of the many papers he sat
|
|
during his formative years.)
|
|
|
|
@c
|
|
Constant AUXF_MAGIC = 0; ! First word holds a safety constant
|
|
Constant AUXF_MAGIC_VALUE = 16339; ! Should be first word of any valid file structure
|
|
Constant AUXF_STATUS = 1; ! One of the following:
|
|
Constant AUXF_STATUS_IS_CLOSED = 1; ! Currently closed, or perhaps doesn't exist
|
|
Constant AUXF_STATUS_IS_OPEN_FOR_READ = 2;
|
|
Constant AUXF_STATUS_IS_OPEN_FOR_WRITE = 3;
|
|
Constant AUXF_STATUS_IS_OPEN_FOR_APPEND = 4;
|
|
Constant AUXF_BINARY = 2; ! False for text files (I7 default), true for binary
|
|
Constant AUXF_STREAM = 3; ! Stream for an open file (meaningless otherwise)
|
|
Constant AUXF_FILENAME = 4; ! Packed address of constant string
|
|
Constant AUXF_IFID_OF_OWNER = 5; ! UUID_ARRAY if owned by this project, or
|
|
! string array of IFID of owner wrapped in //...//, or NULL to leave open
|
|
|
|
@p Instances.
|
|
These structures are not dynamically created: they are precompiled by the NI
|
|
compiler, already filled in with the necessary values. The following command
|
|
generates them.
|
|
|
|
@c
|
|
{-call:PL::Files::arrays}
|
|
|
|
@p Errors.
|
|
This is used for I/O errors of all kinds: it isn't within the Glulx-only
|
|
code because one of the errors is to try to use these routines on the
|
|
Z-machine.
|
|
|
|
@c
|
|
[ FileIO_Error extf err_text struc;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES)) {
|
|
print "^*** Error on unknown file: ", (string) err_text, " ***^";
|
|
} else {
|
|
struc = TableOfExternalFiles-->extf;
|
|
print "^*** Error on file '",
|
|
(string) struc-->AUXF_FILENAME, "': ",
|
|
(string) err_text, " ***^";
|
|
}
|
|
RunTimeProblem(RTP_FILEIOERROR);
|
|
return 0;
|
|
];
|
|
|
|
@p Glulx Material.
|
|
|
|
@c
|
|
#IFDEF TARGET_GLULX;
|
|
|
|
@p Existence.
|
|
Determine whether a file exists on disc. Note that we have no concept of
|
|
directories, or the file system structure on the host machine: indeed, it
|
|
is entirely up to the Glulx VM what it does when asked to look for a file.
|
|
By convention, though, files for a project are stored in the same folder
|
|
as the story file when out in the wild; when a project is developed within
|
|
the Inform user interface, they are either (for preference) stored in a
|
|
|Files| subfolder of the |Materials| folder for a project, or else stored
|
|
alongside the Inform project file.
|
|
|
|
@c
|
|
[ FileIO_Exists extf fref struc rv usage;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES)) rfalse;
|
|
struc = TableOfExternalFiles-->extf;
|
|
if ((struc == 0) || (struc-->AUXF_MAGIC ~= AUXF_MAGIC_VALUE)) rfalse;
|
|
if (struc-->AUXF_BINARY) usage = fileusage_BinaryMode;
|
|
else usage = fileusage_TextMode;
|
|
fref = glk_fileref_create_by_name(fileusage_Data + usage,
|
|
Glulx_ChangeAnyToCString(struc-->AUXF_FILENAME), 0);
|
|
rv = glk_fileref_does_file_exist(fref);
|
|
glk_fileref_destroy(fref);
|
|
return rv;
|
|
];
|
|
|
|
@p Readiness.
|
|
One of our problems is that a file might be being used by another application:
|
|
perhaps even by another story file running in a second incarnation of Glulx,
|
|
like a parallel world of which we can know nothing. We actually want to
|
|
allow for this sort of thing, because one use for external files in I7
|
|
is as a sort of communications conduit for assisting applications.
|
|
|
|
Most operating systems solve this problem by means of locking a file, or
|
|
by creating a second lock-file, the existence of which indicates ownership
|
|
of the original. We haven't got much access to the file-system, though:
|
|
what we do is to set the first character of the file to an asterisk to
|
|
mark it as complete and ready for reading, or to a hyphen to mark it as
|
|
a work in progress.
|
|
|
|
|FileIO_Ready| determines whether or not a file is ready to be read
|
|
from: it has to exist on disc, and to be openable, and also to be ready
|
|
in having this marker asterisk.
|
|
|
|
|FileIO_MarkReady| changes the readiness state of a file, writing the
|
|
asterisk or hyphen into the initial character as needed.
|
|
|
|
@c
|
|
[ FileIO_Ready extf struc fref usage str ch;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES)) rfalse;
|
|
struc = TableOfExternalFiles-->extf;
|
|
if ((struc == 0) || (struc-->AUXF_MAGIC ~= AUXF_MAGIC_VALUE)) rfalse;
|
|
if (struc-->AUXF_BINARY) usage = fileusage_BinaryMode;
|
|
else usage = fileusage_TextMode;
|
|
fref = glk_fileref_create_by_name(fileusage_Data + usage,
|
|
Glulx_ChangeAnyToCString(struc-->AUXF_FILENAME), 0);
|
|
if (glk_fileref_does_file_exist(fref) == false) {
|
|
glk_fileref_destroy(fref);
|
|
rfalse;
|
|
}
|
|
str = glk_stream_open_file(fref, filemode_Read, 0);
|
|
ch = glk_get_char_stream(str);
|
|
glk_stream_close(str, 0);
|
|
glk_fileref_destroy(fref);
|
|
if (ch ~= '*') rfalse;
|
|
rtrue;
|
|
];
|
|
|
|
[ FileIO_MarkReady extf readiness struc fref str ch usage;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES))
|
|
return FileIO_Error(extf, "tried to open a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
if ((struc == 0) || (struc-->AUXF_MAGIC ~= AUXF_MAGIC_VALUE)) rfalse;
|
|
if (struc-->AUXF_BINARY) usage = fileusage_BinaryMode;
|
|
else usage = fileusage_TextMode;
|
|
fref = glk_fileref_create_by_name(fileusage_Data + usage,
|
|
Glulx_ChangeAnyToCString(struc-->AUXF_FILENAME), 0);
|
|
if (glk_fileref_does_file_exist(fref) == false) {
|
|
glk_fileref_destroy(fref);
|
|
return FileIO_Error(extf, "only existing files can be marked");
|
|
}
|
|
if (struc-->AUXF_STATUS ~= AUXF_STATUS_IS_CLOSED) {
|
|
glk_fileref_destroy(fref);
|
|
return FileIO_Error(extf, "only closed files can be marked");
|
|
}
|
|
str = glk_stream_open_file(fref, filemode_ReadWrite, 0);
|
|
glk_stream_set_position(str, 0, 0); ! seek start
|
|
if (readiness) ch = '*'; else ch = '-';
|
|
glk_put_char_stream(str, ch); ! mark as complete
|
|
glk_stream_close(str, 0);
|
|
glk_fileref_destroy(fref);
|
|
];
|
|
|
|
@p Open File.
|
|
|
|
@c
|
|
[ FileIO_Open extf write_flag append_flag
|
|
struc fref str mode ix ch not_this_ifid owner force_header usage;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES))
|
|
return FileIO_Error(extf, "tried to open a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
if ((struc == 0) || (struc-->AUXF_MAGIC ~= AUXF_MAGIC_VALUE)) rfalse;
|
|
if (struc-->AUXF_STATUS ~= AUXF_STATUS_IS_CLOSED)
|
|
return FileIO_Error(extf, "tried to open a file already open");
|
|
if (struc-->AUXF_BINARY) usage = fileusage_BinaryMode;
|
|
else usage = fileusage_TextMode;
|
|
fref = glk_fileref_create_by_name(fileusage_Data + usage,
|
|
Glulx_ChangeAnyToCString(struc-->AUXF_FILENAME), 0);
|
|
if (write_flag) {
|
|
if (append_flag) {
|
|
mode = filemode_WriteAppend;
|
|
if (glk_fileref_does_file_exist(fref) == false)
|
|
force_header = true;
|
|
}
|
|
else mode = filemode_Write;
|
|
} else {
|
|
mode = filemode_Read;
|
|
if (glk_fileref_does_file_exist(fref) == false) {
|
|
glk_fileref_destroy(fref);
|
|
return FileIO_Error(extf, "tried to open a file which does not exist");
|
|
}
|
|
}
|
|
str = glk_stream_open_file(fref, mode, 0);
|
|
glk_fileref_destroy(fref);
|
|
if (str == 0) return FileIO_Error(extf, "tried to open a file but failed");
|
|
struc-->AUXF_STREAM = str;
|
|
if (write_flag) {
|
|
if (append_flag)
|
|
struc-->AUXF_STATUS = AUXF_STATUS_IS_OPEN_FOR_APPEND;
|
|
else
|
|
struc-->AUXF_STATUS = AUXF_STATUS_IS_OPEN_FOR_WRITE;
|
|
glk_stream_set_current(str);
|
|
if ((append_flag == FALSE) || (force_header)) {
|
|
print "- ";
|
|
for (ix=6: ix <= UUID_ARRAY->0: ix++) print (char) UUID_ARRAY->ix;
|
|
print " ", (string) struc-->AUXF_FILENAME, "^";
|
|
}
|
|
} else {
|
|
struc-->AUXF_STATUS = AUXF_STATUS_IS_OPEN_FOR_READ;
|
|
ch = FileIO_GetC(extf);
|
|
if (ch ~= '-' or '*') { jump BadFile; }
|
|
if (ch == '-')
|
|
return FileIO_Error(extf, "tried to open a file which was incomplete");
|
|
ch = FileIO_GetC(extf);
|
|
if (ch ~= ' ') { jump BadFile; }
|
|
ch = FileIO_GetC(extf);
|
|
if (ch ~= '/') { jump BadFile; }
|
|
ch = FileIO_GetC(extf);
|
|
if (ch ~= '/') { jump BadFile; }
|
|
owner = struc-->AUXF_IFID_OF_OWNER;
|
|
ix = 3;
|
|
if (owner == UUID_ARRAY) ix = 8;
|
|
if (owner ~= NULL) {
|
|
for (: ix <= owner->0: ix++) {
|
|
ch = FileIO_GetC(extf);
|
|
if (ch == -1) { jump BadFile; }
|
|
if (ch ~= owner->ix) not_this_ifid = true;
|
|
if (ch == ' ') break;
|
|
}
|
|
if (not_this_ifid == false) {
|
|
ch = FileIO_GetC(extf);
|
|
if (ch ~= ' ') { jump BadFile; }
|
|
}
|
|
}
|
|
while (ch ~= -1) {
|
|
ch = FileIO_GetC(extf);
|
|
if (ch == 10 or 13) break;
|
|
}
|
|
if (not_this_ifid) {
|
|
struc-->AUXF_STATUS = AUXF_STATUS_IS_CLOSED;
|
|
glk_stream_close(str, 0);
|
|
return FileIO_Error(extf,
|
|
"tried to open a file owned by another project");
|
|
}
|
|
}
|
|
return struc-->AUXF_STREAM;
|
|
.BadFile;
|
|
struc-->AUXF_STATUS = AUXF_STATUS_IS_CLOSED;
|
|
glk_stream_close(str, 0);
|
|
return FileIO_Error(extf, "tried to open a file which seems to be malformed");
|
|
];
|
|
|
|
@p Close File.
|
|
Note that a call to the following, in write mode, must be followed by a
|
|
|glk_stream_set_current()|, or else the next print statement will run into
|
|
Glk errors.
|
|
|
|
@c
|
|
[ FileIO_Close extf struc;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES))
|
|
return FileIO_Error(extf, "tried to open a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
if (struc-->AUXF_STATUS ~=
|
|
AUXF_STATUS_IS_OPEN_FOR_READ or
|
|
AUXF_STATUS_IS_OPEN_FOR_WRITE or
|
|
AUXF_STATUS_IS_OPEN_FOR_APPEND)
|
|
return FileIO_Error(extf, "tried to close a file which is not open");
|
|
if (struc-->AUXF_STATUS ==
|
|
AUXF_STATUS_IS_OPEN_FOR_WRITE or
|
|
AUXF_STATUS_IS_OPEN_FOR_APPEND) {
|
|
glk_stream_set_position(struc-->AUXF_STREAM, 0, 0); ! seek start
|
|
glk_put_char_stream(struc-->AUXF_STREAM, '*'); ! mark as complete
|
|
}
|
|
glk_stream_close(struc-->AUXF_STREAM, 0);
|
|
struc-->AUXF_STATUS = AUXF_STATUS_IS_CLOSED;
|
|
];
|
|
|
|
@p Get Character.
|
|
|
|
@c
|
|
[ FileIO_GetC extf struc;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES)) return -1;
|
|
struc = TableOfExternalFiles-->extf;
|
|
if (struc-->AUXF_STATUS ~= AUXF_STATUS_IS_OPEN_FOR_READ) return -1;
|
|
return glk_get_char_stream(struc-->AUXF_STREAM);
|
|
];
|
|
|
|
@p Put Character.
|
|
|
|
@c
|
|
[ FileIO_PutC extf char struc;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES)) return -1;
|
|
return FileIO_Error(extf, "tried to write to a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
if (struc-->AUXF_STATUS ~=
|
|
AUXF_STATUS_IS_OPEN_FOR_WRITE or
|
|
AUXF_STATUS_IS_OPEN_FOR_APPEND)
|
|
return FileIO_Error(extf,
|
|
"tried to write to a file which is not open for writing");
|
|
return glk_put_char_stream(struc-->AUXF_STREAM, char);
|
|
];
|
|
|
|
@p Print Line.
|
|
We read characters from the supplied file until the next newline
|
|
character. (We allow for that to be encoded as either a single |0a| or a
|
|
single |0d|.) Each character is printed, and at the end we print a newline.
|
|
|
|
@c
|
|
[ FileIO_PrintLine extf ch struc;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES))
|
|
return FileIO_Error(extf, "tried to write to a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
for (::) {
|
|
ch = FileIO_GetC(extf);
|
|
if (ch == -1) rfalse;
|
|
if (ch == 10 or 13) { print "^"; rtrue; }
|
|
print (char) ch;
|
|
}
|
|
];
|
|
|
|
@p Print Contents.
|
|
Repeating this until the file runs out is equivalent to the Unix command
|
|
|cat|, that is, it copies the stream of characters from the file to the
|
|
output stream. (This might well be another file, just as with |cat|, in
|
|
which case we have a copy utility.)
|
|
|
|
@c
|
|
[ FileIO_PrintContents extf tab struc;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES))
|
|
return FileIO_Error(extf, "tried to access a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
if (struc-->AUXF_BINARY)
|
|
return FileIO_Error(extf, "printing text will not work with binary files");
|
|
if (FileIO_Open(extf, false) == 0) rfalse;
|
|
while (FileIO_PrintLine(extf)) ;
|
|
FileIO_Close(extf);
|
|
rtrue;
|
|
];
|
|
|
|
@p Print Text.
|
|
The following writes a given piece of text as the new content of the file,
|
|
either as the whole file (if |append_flag| is false) or adding only to the
|
|
end (if true).
|
|
|
|
@c
|
|
[ FileIO_PutContents extf text append_flag struc str ch oldstream;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES))
|
|
return FileIO_Error(extf, "tried to access a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
if (struc-->AUXF_BINARY)
|
|
return FileIO_Error(extf, "writing text will not work with binary files");
|
|
oldstream = glk_stream_get_current();
|
|
str = FileIO_Open(extf, true, append_flag);
|
|
if (str == 0) rfalse;
|
|
@push say__p; @push say__pc;
|
|
ClearParagraphing(19);
|
|
TEXT_TY_Say(text);
|
|
FileIO_Close(extf);
|
|
if (oldstream) glk_stream_set_current(oldstream);
|
|
@pull say__pc; @pull say__p;
|
|
rfalse;
|
|
];
|
|
|
|
@p Serialising Tables.
|
|
The most important data structures to "serialise" -- that is, to convert
|
|
from their binary representations in memory into text representations in an
|
|
external file -- are Tables. Here we only carry out the file-handling; the
|
|
actual translations are in "Tables.i6t".
|
|
|
|
@c
|
|
[ FileIO_PutTable extf tab rv struc oldstream;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES))
|
|
return FileIO_Error(extf, "tried to write table to a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
if (struc-->AUXF_BINARY)
|
|
return FileIO_Error(extf, "writing a table will not work with binary files");
|
|
oldstream = glk_stream_get_current();
|
|
if (FileIO_Open(extf, true) == 0) rfalse;
|
|
rv = TablePrint(tab);
|
|
FileIO_Close(extf);
|
|
if (oldstream) glk_stream_set_current(oldstream);
|
|
if (rv) return RunTimeProblem(RTP_TABLE_CANTSAVE, tab);
|
|
rtrue;
|
|
];
|
|
|
|
[ FileIO_GetTable extf tab struc;
|
|
if ((extf < 1) || (extf > NO_EXTERNAL_FILES))
|
|
return FileIO_Error(extf, "tried to read table from a non-file");
|
|
struc = TableOfExternalFiles-->extf;
|
|
if (struc-->AUXF_BINARY)
|
|
return FileIO_Error(extf, "reading a table will not work with binary files");
|
|
if (FileIO_Open(extf, false) == 0) rfalse;
|
|
TableRead(tab, extf);
|
|
FileIO_Close(extf);
|
|
rtrue;
|
|
];
|
|
|
|
@p Z-Machine Stubs.
|
|
These routines do the minimum possible, but equally, they only generate a
|
|
run-time problem when there is no alternative.
|
|
|
|
@c
|
|
#IFNOT; ! TARGET_GLULX
|
|
[ FileIO_Exists extf; rfalse; ];
|
|
[ FileIO_Ready extf; rfalse; ];
|
|
[ FileIO_GetC extf; return -1; ];
|
|
[ FileIO_PutTable extf tab;
|
|
return FileIO_Error(extf, "external files can only be used under Glulx");
|
|
];
|
|
[ FileIO_MarkReady extf status; FileIO_PutTable(extf); ];
|
|
[ FileIO_GetTable extf tab; FileIO_PutTable(extf); ];
|
|
[ FileIO_PrintContents extf; FileIO_PutTable(extf); ];
|
|
[ FileIO_PutContents extf; FileIO_PutTable(extf); ];
|
|
#ENDIF; ! TARGET_GLULX
|
|
|
|
@p Back To Core.
|
|
|
|
@c
|
|
#IFNOT; ! PLUGIN_FILES
|
|
[ FileIO_GetC extf; return -1; ];
|
|
#ENDIF; ! PLUGIN_FILES
|