File and compression routines

The following routines implement a fast buffered file I/O system, which supports the reading and writing of compressed files using a ring buffer algorithm based on the LZSS compressor by Haruhiko Okumura. This does not achieve quite such good compression as programs like zip and lha, but unpacking is very fast and it does not require much memory. Packed files always begin with the 32 bit value F_PACK_MAGIC, and autodetect files with the value F_NOPACK_MAGIC.

The following FA_* flags are guaranteed to work: FA_RDONLY, FA_HIDDEN, FA_SYSTEM, FA_LABEL, FA_DIREC, FA_ARCH. Do not use any other flags from DOS/Windows or your code will not compile on another platform. Flags FA_SYSTEM, FA_LABEL and FA_ARCH are valuable only on DOS/Windows (entries with system flag, volume labels and archive flag). FA_RDONLY is for directory entries with read-only flag on DOS-like systems or unwritable by current user on Unix-like systems. FA_HIDDEN is for entries with hidden flag on DOS-like systems or starting with '.' on Unix (dotted files - excluding '.' and '..'). FA_DIREC represents directories. Flags can be combined using '|' (binary OR operator).

When passed to the functions as the 'attrib' parameter, these flags represent an upper set in which the actual flag set of a matching file must be included. That is, in order for a file to be matching, its attributes may contain any of the specified flags but must not contain any of the unspecified flags. Thus, if you pass 'FA_DIREC | FA_RDONLY', normal files and directories will be included as well as read-only files and directories, but not hidden files and directories. Similarly, if you pass 'FA_ARCH' then both archived and non-archived files will be included.

void get_executable_name(char *buf, int size);
Fills buf with the full path to the current executable, writing at most size bytes. This generally comes from argv[0], but on Unix systems if argv[0] does not specify the path, we search for our file in $PATH.

char *fix_filename_case(char *path);
Converts a filename to a standardised case. On DOS platforms, they will be entirely uppercase. Returns a copy of the path parameter.

char *fix_filename_slashes(char *path);
Converts all the directory separators in a filename to a standard character. On DOS platforms, this is a backslash. Returns a copy of the path parameter.

char *fix_filename_path(char *dest, const char *path, int size);
Converts a partial filename into a full path, storing at most size bytes into the dest buffer. Returns a copy of the dest parameter.

char *replace_filename(char *dest, const char *path, const char *filename, int size);
Replaces the specified path+filename with a new filename tail, storing at most size bytes into the dest buffer. Returns a copy of the dest parameter.

char *replace_extension(char *dest, const char *filename, const char *ext, int size);
Replaces the specified filename+extension with a new extension tail, storing at most size bytes into the dest buffer. Returns a copy of the dest parameter.

char *append_filename(char *dest, const char *path, const char *filename, int size);
Concatenates the specified filename onto the end of the specified path, storing at most size bytes into the dest buffer. Returns a copy of the dest parameter.

char *get_filename(const char *path);
When passed a completely specified file path, this returns a pointer to the filename portion. Both '\' and '/' are recognized as directory separators.

char *get_extension(const char *filename);
When passed a complete filename (with or without path information) this returns a pointer to the file extension.

void put_backslash(char *filename);
If the last character of the filename is not a '\', '/', '#' or a device separator (ie. ':' under DOS), this routine will concatenate either a '\' or '/' on to it (depending on the platform). Note: ignore the function name, it's out of date.

int file_exists(const char *filename, int attrib, int *aret);
Checks whether a file matching the given name and attributes (see above) exists, returning non-zero if it does. If aret is not NULL, it will be set to the attributes of the matching file. If an error occurs the system error code will be stored in errno.

int exists(const char *filename);
Shortcut version of file_exists(), which checks for normal files, which may have the archive or read-only bits set, but are not hidden, directories, system files, etc.

long file_size(const char *filename);
Returns the size of a file, in bytes. If the file does not exist or an error occurs, it will return zero and store the system error code in errno.

time_t file_time(const char *filename);
Returns the modification time (number of seconds since 00:00:00 GMT 1/1/1970) of a file.

int delete_file(const char *filename);
Removes a file from the disk.

int for_each_file(const char *name, int attrib, void (*callback)(const char *filename, int attrib, int param), int param);
Finds all the files on the disk which match the given wildcard specification and file attributes (see above), and executes callback() once for each. callback() will be passed three arguments, the first a string which contains the completed filename, the second being the attributes of the file, and the third an int which is simply a copy of param (you can use this for whatever you like). If an error occurs an error code will be stored in errno, and callback() can cause for_each_file() to abort by setting errno itself. Returns the number of successful calls made to callback().

int al_findfirst(const char *pattern, struct al_ffblk *info, int attrib);
Low-level function for searching files. This function finds the first file which matches the given wildcard specification and file attributes (see above). The information about the file (if any) will be put in the al_ffblk structure which you have to provide. The function returns zero if a match is found, nonzero if none is found or if an error occured and, in the latter case, sets errno accordingly. The al_ffblk structure looks like:

  struct al_ffblk
      int attrib;       - actual attributes of the file found
      time_t time;      - modification time of file
      long size;        - size of file
      char name[512];   - name of file

There is some other stuff in the structure as well, but it is there for internal use only.

int al_findnext(struct al_ffblk *info);
This finds the next file in a search started by al_findfirst(). Returns zero if a match is found, nonzero if none is found or if an error occured and, in the latter case, sets errno accordingly.

void al_findclose(struct al_ffblk *info);
This closes a previously opened search with al_findfirst().

int find_allegro_resource(char *dest, const char *resource, const char *ext, const char *datafile, const char *objectname, const char *envvar, const char *subdir, int size);
Searches for a support file, eg. allegro.cfg or language.dat. Passed a resource string describing what you are looking for, along with extra optional information such as the default extension, what datafile to look inside, what the datafile object name is likely to be, any special environment variable to check, and any subdirectory that you would like to check as well as the default location, this function looks in a hell of a lot of different places :-) Returns zero on success, and stores a full path to the file (at most size bytes) into the dest buffer.

void packfile_password(const char *password);
Sets the encryption password to be used for all read/write operations on files opened in future using Allegro's packfile functions (whether they are compressed or not), including all the save, load and config routines. Files written with an encryption password cannot be read unless the same password is selected, so be careful: if you forget the key, I can't make your data come back again! Pass NULL or an empty string to return to the normal, non-encrypted mode. If you are using this function to prevent people getting access to your datafiles, be careful not to store an obvious copy of the password in your executable: if there are any strings like "I'm the password for the datafile", it would be fairly easy to get access to your data :-)

Note #1: when writing a packfile, you can change the password to whatever you want after opening the file, without affecting the write operation. On the contrary, when writing a sub-chunk of a packfile, you must make sure that the password that was active at the time the sub-chunk was opened is still active before closing the sub-chunk. This is guaranteed to be true if you didn't call the packfile_password() routine in the meantime. Read operations, either on packfiles or sub-chunks, have no such restriction.

Note #2: as explained above, the password is used for all read/write operations on files, including for several functions of the library that operate on files without explicitly using packfiles, e.g load_bitmap(). The unencrypted mode is mandatory in order for those functions to work. Therefore remember to call packfile_password(NULL) before using them if you previously changed the password. As a rule of thumb, always call packfile_password(NULL) when you are done with operations on packfiles.

PACKFILE *pack_fopen(const char *filename, const char *mode);
Opens a file according to mode, which may contain any of the flags:

  • 'r' - open file for reading.

  • 'w' - open file for writing, overwriting any existing data.

  • 'p' - open file in packed mode. Data will be compressed as it is written to the file, and automatically uncompressed during read operations. Files created in this mode will produce garbage if they are read without this flag being set.

  • '!' - open file for writing in normal, unpacked mode, but add the value F_NOPACK_MAGIC to the start of the file, so that it can later be opened in packed mode and Allegro will automatically detect that the data does not need to be decompressed.

Instead of these flags, one of the constants F_READ, F_WRITE, F_READ_PACKED, F_WRITE_PACKED or F_WRITE_NOPACK may be used as the mode parameter. On success, pack_fopen() returns a pointer to a file structure, and on error it returns NULL and stores an error code in errno. An attempt to read a normal file in packed mode will cause errno to be set to EDOM.

The packfile functions also understand several "magic" filenames that are used for special purposes. These are:

  • "#" - read data that has been appended to your executable file with the exedat utility, as if it was a regular independent disk file.

  • 'filename.dat#object_name' - open a specific object from a datafile, and read from it as if it was a regular file. You can treat nested datafiles exactly like a normal directory structure, for example you could open 'filename.dat#graphics/level1/mapdata'.

  • '#object_name' - combination of the above, reading an object from a datafile that has been appended onto your executable.

With these special filenames, the contents of a datafile object or appended file can be read in an identical way to a normal disk file, so any of the file access functions in Allegro (eg. load_pcx() and set_config_file()) can be used to read from them. Note that you can't write to these special files, though: the fake file is read only. Also, you must save your datafile uncompressed or with per-object compression if you are planning on loading individual objects from it (otherwise there will be an excessive amount of seeking when it is read). Finally, be aware that the special Allegro object types aren't the same format as the files you import the data from. When you import data like bitmaps or samples into the grabber, they are converted into a special Allegro-specific format, but the '#' marker file syntax reads the objects as raw binary chunks. This means that if, for example, you want to use load_pcx to read an image from a datafile, you should import it as a binary block rather than as a BITMAP object.

int pack_fclose(PACKFILE *f);
int pack_fseek(PACKFILE *f, int offset);
int pack_feof(PACKFILE *f);
int pack_ferror(PACKFILE *f);
int pack_getc(PACKFILE *f);
int pack_putc(int c, PACKFILE *f);
int pack_igetw(PACKFILE *f);
long pack_igetl(PACKFILE *f);
int pack_iputw(int w, PACKFILE *f);
long pack_iputl(long l, PACKFILE *f);
int pack_mgetw(PACKFILE *f);
long pack_mgetl(PACKFILE *f);
int pack_mputw(int w, PACKFILE *f);
long pack_mputl(long l, PACKFILE *f);
long pack_fread(void *p, long n, PACKFILE *f);
long pack_fwrite(const void *p, long n, PACKFILE *f);
char *pack_fgets(char *p, int max, PACKFILE *f);
int pack_fputs(const char *p, PACKFILE *f);

These work like the equivalent stdio functions. There are some differences, however:

  • Seeking only supports forward movement relative to the current position. Note that seeking is very slow when reading compressed files, and so should be avoided unless you are sure that the file is not compressed.

  • The pack_i* and pack_m* routines read and write 16 and 32 bit values using the Intel and Motorola byte ordering systems (endianness) respectively. Intel is least significant byte first (little-endian); Motorola is most significant byte first (big-endian).

  • pack_fread() and pack_fwrite() take a single size parameter instead of that silly size and num_elements system.

  • The pack_fgets() function does not include a trailing carriage return in the returned string.

  • pack_fputs() always writes in the UTF-8 text encoding format, converting from the current text encoding. Newlines (\n) are written as \r\n on DOS/Windows. If you do not want either of these things to happen, use pack_fwrite() and/or pack_putc() instead.

PACKFILE *pack_fopen_chunk(PACKFILE *f, int pack);
Opens a sub-chunk of a file. Chunks are primarily intended for use by the datafile code, but they may also be useful for your own file routines. A chunk provides a logical view of part of a file, which can be compressed as an individual entity and will automatically insert and check length counts to prevent reading past the end of the chunk. To write a chunk to the file f, use the code:

      /* assumes f is a PACKFILE * which has been opened */
      f = pack_fopen_chunk(f, pack);    /* in write mode */
      write some data to f
      f = pack_fclose_chunk(f);

The data written to the chunk will be prefixed with two length counts (32 bit, big-endian). For uncompressed chunks these will both be set to the size of the data in the chunk. For compressed chunks (created by setting the pack flag), the first length will be the raw size of the chunk, and the second will be the negative size of the uncompressed data.

To read the chunk, use the code:

      /* assumes f is a PACKFILE * which has been opened */
      f = pack_fopen_chunk(f, FALSE);    */ in read mode */
      read data from f
      f = pack_fclose_chunk(f);

This sequence will read the length counts created when the chunk was written, and automatically decompress the contents of the chunk if it was compressed. The length will also be used to prevent reading past the end of the chunk (Allegro will return EOF if you attempt this), and to automatically skip past any unread chunk data when you call pack_fclose_chunk().

Chunks can be nested inside each other by making repeated calls to pack_fopen_chunk(). When writing a file, the compression status is inherited from the parent file, so you only need to set the pack flag if the parent is not compressed but you want to pack the chunk data. If the parent file is already open in packed mode, setting the pack flag will result in data being compressed twice: once as it is written to the chunk, and again as the chunk passes it on to the parent file.

PACKFILE *pack_fclose_chunk(PACKFILE *f);
Closes a sub-chunk of a file, previously obtained by calling pack_fopen_chunk().