0ad/source/lib/res/file/vfs.cpp
janwas b8c131c431 # housekeeping
* zip.h removed old test symbols
* path_util fixed bug in mixed-separating-slash handling
* renamed ERR_OK to INFO_OK; allows searching for "return ERR_" to yield
all actual errors (instead of having to filter out ERR_OK)

This was SVN commit r3945.
2006-06-04 22:27:40 +00:00

768 lines
22 KiB
C++

/**
* =========================================================================
* File : vfs.cpp
* Project : 0 A.D.
* Description : Handle-based wrapper on top of the vfs_mount API.
*
* @author Jan.Wassenberg@stud.uni-karlsruhe.de
* =========================================================================
*/
/*
* Copyright (c) 2004-2005 Jan Wassenberg
*
* Redistribution and/or modification are also permitted under the
* terms of the GNU General Public License as published by the
* Free Software Foundation (version 2 or later, at your option).
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
#include "precompiled.h"
#include <string.h>
#include <time.h>
#include <math.h>
#include <map>
#include <list>
#include <deque>
#include <vector>
#include <string>
#include <algorithm>
#include "lib/lib.h"
#include "lib/adts.h"
#include "lib/timer.h"
#include "lib/res/res.h"
#include "lib/sysdep/dir_watch.h"
#include "file_internal.h"
// not safe to call before main!
// pathnames are case-insensitive.
// implementation:
// when mounting, we get the exact filenames as reported by the OS;
// we allow open requests with mixed case to match those,
// but still use the correct case when passing to other libraries
// (e.g. the actual open() syscall, called via file_open).
// rationale:
// necessary, because some exporters output .EXT uppercase extensions
// and it's unreasonable to expect that users will always get it right.
// rationale for no forcibly-close support:
// issue:
// we might want to edit files while the game has them open.
// usual case: edit file, notify engine that it should be reloaded.
// here: need to tell the engine to stop what it's doing and close the file;
// only then can the artist write to the file, and trigger a reload.
//
// work involved:
// since closing a file with pending aios results in undefined
// behavior on Win32, we would have to keep track of all aios from each file,
// and cancel them. we'd also need to notify the higher level resource user
// that its read was cancelled, as opposed to failing due to read errors
// (which might cause the game to terminate).
//
// this is just more work than benefit. cases where the game holds on to files
// are rare:
// - streaming music (artist can use regular commands to stop the current
// track, or all music)
// - if the engine happens to be reading that file at the moment (expected
// to happen only during loading, and these are usually one-shot anway,
// i.e. it'll be done soon)
// - bug (someone didn't close a file - tough luck, and should be fixed
// instead of hacking around it).
// - archives (these remain open. allowing reload would mean we'd have to keep
// track of all files from an archive, and reload them all. another hassle.
// anyway, if files are to be changed in-game, then change the plain-file
// version - that's what they're for).
///////////////////////////////////////////////////////////////////////////////
//
// directory
//
///////////////////////////////////////////////////////////////////////////////
struct VDir
{
DirIterator di;
uint di_valid : 1; // <di> will be closed iff == 1
};
H_TYPE_DEFINE(VDir);
static void VDir_init(VDir* UNUSED(vd), va_list UNUSED(args))
{
}
static void VDir_dtor(VDir* vd)
{
// note: DirIterator has no way of checking if it's valid;
// we must therefore only free it if reload() succeeded.
if(vd->di_valid)
{
xdir_close(&vd->di);
vd->di_valid = 0;
}
}
static LibError VDir_reload(VDir* vd, const char* V_dir_path, Handle UNUSED(hvd))
{
debug_assert(VFS_PATH_IS_DIR(V_dir_path));
RETURN_ERR(xdir_open(V_dir_path, &vd->di));
vd->di_valid = 1;
return INFO_OK;
}
static LibError VDir_validate(const VDir* vd)
{
// note: <di> is mostly opaque and cannot be validated.
if(vd->di.filter && !isprint(vd->di.filter[0]))
WARN_RETURN(ERR_1);
return INFO_OK;
}
static LibError VDir_to_string(const VDir* vd, char* buf)
{
const char* filter = vd->di.filter;
if(!vd->di.filter_latched)
filter = "?";
if(!filter)
filter = "*";
snprintf(buf, H_STRING_LEN, "(\"%s\")", filter);
return INFO_OK;
}
// open a directory for reading its entries via vfs_next_dirent.
// <v_dir> need not end in '/'; we add it if not present.
Handle vfs_dir_open(const char* V_dir_path)
{
// must disallow handle caching because this object is not
// copy-equivalent (since the iterator is advanced by each user).
return h_alloc(H_VDir, V_dir_path, RES_NO_CACHE);
}
// close the handle to a directory.
LibError vfs_dir_close(Handle& hd)
{
return h_free(hd, H_VDir);
}
// retrieve the next (order is unspecified) dir entry matching <filter>.
// return 0 on success, ERR_DIR_END if no matching entry was found,
// or a negative error code on failure.
// filter values:
// - 0: anything;
// - "/": any subdirectory;
// - "/|<pattern>": any subdirectory, or as below with <pattern>;
// - <pattern>: any file whose name matches; ? and * wildcards are allowed.
//
// note that the directory entries are only scanned once; after the
// end is reached (-> ERR_DIR_END returned), no further entries can
// be retrieved, even if filter changes (which shouldn't happen - see impl).
//
// rationale: we do not sort directory entries alphabetically here.
// most callers don't need it and the overhead is considerable
// (we'd have to store all entries in a vector). it is left up to
// higher-level code such as VfsUtil.
LibError vfs_dir_next_ent(const Handle hd, DirEnt* ent, const char* filter)
{
H_DEREF(hd, VDir, vd);
return dir_filtered_next_ent(&vd->di, ent, filter);
}
///////////////////////////////////////////////////////////////////////////////
//
// file
//
///////////////////////////////////////////////////////////////////////////////
// return actual path to the specified file:
// "<real_directory>/fn" or "<archive_name>/fn".
LibError vfs_realpath(const char* V_path, char* realpath)
{
TFile* tf;
CHECK_ERR(tree_lookup(V_path, &tf));
const char* atom_fn = tfile_get_atom_fn(tf);
const Mount* m = tfile_get_mount(tf);
return mount_realpath(atom_fn, m, realpath);
}
// does the specified file exist? return false on error.
// useful because a "file not found" warning is not raised, unlike vfs_stat.
bool vfs_exists(const char* V_fn)
{
TFile* tf;
return (tree_lookup(V_fn, &tf) == 0);
}
// get file status (mode, size, mtime). output param is zeroed on error.
LibError vfs_stat(const char* V_path, struct stat* s)
{
memset(s, 0, sizeof(*s));
TFile* tf;
CHECK_ERR(tree_lookup(V_path, &tf));
return tree_stat(tf, s);
}
//-----------------------------------------------------------------------------
struct VFile
{
File f;
// current file pointer. this is necessary because file.cpp's interface
// requires passing an offset for every VIo; see file_io_issue.
off_t ofs;
// pointer to VFS file info storage; used to update size/mtime
// after a newly written file is closed.
TFile* tf;
uint is_valid : 1;
// be aware when adding fields that this struct is quite large,
// and may require increasing the control block size limit.
};
H_TYPE_DEFINE(VFile);
static void VFile_init(VFile* vf, va_list args)
{
vf->f.flags = va_arg(args, int);
}
static void VFile_dtor(VFile* vf)
{
// note: checking if reload() succeeded is unnecessary because
// xfile_close and mem_free_h safely handle 0-initialized data.
WARN_ERR(xfile_close(&vf->f));
// update file state in VFS tree
// (must be done after close, since that calculates the size)
if(vf->f.flags & FILE_WRITE)
tree_update_file(vf->tf, vf->f.size, time(0)); // can't fail
if(vf->is_valid)
stats_close();
}
static LibError VFile_reload(VFile* vf, const char* V_path, Handle)
{
const uint flags = vf->f.flags;
// we're done if file is already open. need to check this because
// reload order (e.g. if resource opens a file) is unspecified.
if(xfile_is_open(&vf->f))
return INFO_OK;
TFile* tf;
uint lf = (flags & FILE_WRITE)? LF_CREATE_MISSING : 0;
LibError err = tree_lookup(V_path, &tf, lf);
if(err < 0)
{
// don't CHECK_ERR - this happens often and the dialog is annoying
debug_printf("lookup failed for %s\n", V_path);
return err;
}
// careful! FILE_WRITE_TO_TARGET consists of 2 bits; they must both be
// set (one of them is FILE_WRITE, which can be set independently).
// this is a bit ugly but better than requiring users to write
// FILE_WRITE|FILE_WRITE_TO_TARGET.
if((flags & FILE_WRITE_TO_TARGET) == FILE_WRITE_TO_TARGET)
RETURN_ERR(set_mount_to_write_target(tf));
RETURN_ERR(xfile_open(V_path, flags, tf, &vf->f));
stats_open(vf->f.atom_fn, vf->f.size);
vf->is_valid = 1;
vf->tf = tf;
return INFO_OK;
}
static LibError VFile_validate(const VFile* vf)
{
// <ofs> doesn't have any invariant we can check.
RETURN_ERR(xfile_validate(&vf->f));
return INFO_OK;
}
static LibError VFile_to_string(const VFile* UNUSED(vf), char* buf)
{
strcpy(buf, ""); // safe
return INFO_OK;
}
// return the size of an already opened file, or a negative error code.
ssize_t vfs_size(Handle hf)
{
H_DEREF(hf, VFile, vf);
return vf->f.size;
}
// open the file for synchronous or asynchronous VIo. write access is
// requested via FILE_WRITE flag, and is not possible for files in archives.
// file_flags: default 0
//
// on failure, a debug_warn is generated and a negative error code returned.
Handle vfs_open(const char* V_fn, uint file_flags)
{
// keeping files open doesn't make sense in most cases (because the
// file is used to load resources, which are cached at a higher level).
uint res_flags = RES_NO_CACHE;
// res_flags is for h_alloc and file_flags goes to VFile_init.
// h_alloc already complains on error.
return h_alloc(H_VFile, V_fn, res_flags, file_flags);
}
// close the handle to a file.
LibError vfs_close(Handle& hf)
{
// h_free already complains on error.
return h_free(hf, H_VFile);
}
// transfer the next <size> bytes to/from the given file.
// (read or write access was chosen at file-open time).
//
// if non-NULL, <cb> is called for each block transferred, passing <ctx>.
// it returns how much data was actually transferred, or a negative error
// code (in which case we abort the transfer and return that value).
// the callback mechanism is useful for user progress notification or
// processing data while waiting for the next I/O to complete
// (quasi-parallel, without the complexity of threads).
//
// p (value-return) indicates the buffer mode:
// - *p == 0: read into buffer we allocate; set *p.
// caller should mem_free it when no longer needed.
// - *p != 0: read into or write into the buffer *p.
// - p == 0: only read into temp buffers. useful if the callback
// is responsible for processing/copying the transferred blocks.
// since only temp buffers can be added to the cache,
// this is the preferred read method.
//
// return number of bytes transferred (see above), or a negative error code.
ssize_t vfs_io(const Handle hf, const size_t size, FileIOBuf* pbuf,
FileIOCB cb, uintptr_t cb_ctx)
{
debug_printf("VFS| io: size=%d\n", size);
H_DEREF(hf, VFile, vf);
File* f = &vf->f;
stats_io_user_request(size);
trace_notify_io(f->atom_fn, size, f->flags);
off_t ofs = vf->ofs;
vf->ofs += (off_t)size;
ssize_t nbytes = xfile_io(&vf->f, ofs, size, pbuf, cb, cb_ctx);
RETURN_ERR(nbytes);
return nbytes;
}
// load the entire file <fn> into memory.
// p and size are filled with address/size of buffer (0 on failure).
// flags influences IO mode and is typically 0.
// when the file contents are no longer needed, call file_buf_free(buf).
LibError vfs_load(const char* V_fn, FileIOBuf& buf, size_t& size,
uint file_flags, FileIOCB cb, uintptr_t cb_ctx) // all default 0
{
debug_printf("VFS| load: V_fn=%s\n", V_fn);
const char* atom_fn = file_make_unique_fn_copy(V_fn);
const uint fb_flags = (file_flags & FILE_LONG_LIVED)? FB_LONG_LIVED : 0;
buf = file_cache_retrieve(atom_fn, &size, fb_flags);
if(buf)
{
// we want to skip the below code (especially vfs_open) for
// efficiency. that includes stats/trace accounting, though,
// so duplicate that here:
stats_cache(CR_HIT, size, atom_fn);
stats_io_user_request(size);
trace_notify_io(atom_fn, size, file_flags);
size_t actual_size;
LibError ret = file_io_call_back(buf, size, cb, cb_ctx, actual_size);
if(ret < 0)
file_buf_free(buf);
// we don't care if the cb has "had enough" or whether it would
// accept more data - this is all it gets and we need to
// translate return value to avoid confusing callers.
if(ret == INFO_CB_CONTINUE)
ret = INFO_OK;
size = actual_size;
return ret;
}
buf = 0; size = 0; // initialize in case something below fails
Handle hf = vfs_open(atom_fn, file_flags);
H_DEREF(hf, VFile, vf);
size = vf->f.size;
buf = FILE_BUF_ALLOC;
ssize_t nread = vfs_io(hf, size, &buf, cb, cb_ctx);
// IO failed
if(nread < 0)
{
file_buf_free(buf);
(void)vfs_close(hf);
buf = 0, size = 0; // make sure they are zeroed
return (LibError)nread;
}
debug_assert(nread == (ssize_t)size);
(void)file_cache_add(buf, size, atom_fn, file_flags);
stats_cache(CR_MISS, size, atom_fn);
(void)vfs_close(hf);
return INFO_OK;
}
// caveat: pads file to next max(4kb, sector_size) boundary
// (due to limitation of Win32 FILE_FLAG_NO_BUFFERING I/O).
// if that's a problem, specify FILE_NO_AIO when opening.
ssize_t vfs_store(const char* V_fn, const void* p, const size_t size, uint flags /* default 0 */)
{
Handle hf = vfs_open(V_fn, flags|FILE_WRITE);
H_DEREF(hf, VFile, vf);
FileIOBuf buf = (FileIOBuf)p;
const ssize_t ret = vfs_io(hf, size, &buf);
WARN_ERR(vfs_close(hf));
return ret;
}
///////////////////////////////////////////////////////////////////////////////
//
// asynchronous I/O
//
///////////////////////////////////////////////////////////////////////////////
// we don't support forcibly closing files => don't need to keep track of
// all IOs pending for each file. too much work, little benefit.
struct VIo
{
Handle hf;
size_t size;
void* buf;
FileIo io;
};
H_TYPE_DEFINE(VIo);
static void VIo_init(VIo* vio, va_list args)
{
vio->hf = va_arg(args, Handle);
vio->size = va_arg(args, size_t);
vio->buf = va_arg(args, void*);
}
static void VIo_dtor(VIo* vio)
{
// note: checking if reload() succeeded is unnecessary because
// xfile_io_discard safely handles 0-initialized data.
WARN_ERR(xfile_io_discard(&vio->io));
}
// we don't support transparent read resume after file invalidation.
// if the file has changed, we'd risk returning inconsistent data.
// doesn't look possible without controlling the AIO implementation:
// when we cancel, we can't prevent the app from calling
// aio_result, which would terminate the read.
static LibError VIo_reload(VIo* vio, const char* UNUSED(fn), Handle UNUSED(h))
{
size_t size = vio->size;
void* buf = vio->buf;
H_DEREF(vio->hf, VFile, vf);
off_t ofs = vf->ofs;
vf->ofs += (off_t)size;
return xfile_io_issue(&vf->f, ofs, size, buf, &vio->io);
}
static LibError VIo_validate(const VIo* vio)
{
if(vio->hf < 0)
WARN_RETURN(ERR_21);
// <size> doesn't have any invariant we can check.
if(debug_is_pointer_bogus(vio->buf))
WARN_RETURN(ERR_22);
return xfile_io_validate(&vio->io);
}
static LibError VIo_to_string(const VIo* vio, char* buf)
{
snprintf(buf, H_STRING_LEN, "buf=%p size=%d", vio->buf, vio->size);
return INFO_OK;
}
// begin transferring <size> bytes, starting at <ofs>. get result
// with vfs_io_wait; when no longer needed, free via vfs_io_discard.
Handle vfs_io_issue(Handle hf, size_t size, void* buf)
{
const char* fn = 0;
uint flags = 0;
return h_alloc(H_VIo, fn, flags, hf, size, buf);
}
// finished with transfer <hio> - free its buffer (returned by vfs_io_wait)
LibError vfs_io_discard(Handle& hio)
{
return h_free(hio, H_VIo);
}
// indicates if the VIo referenced by <io> has completed.
// return value: 0 if pending, 1 if complete, < 0 on error.
int vfs_io_has_completed(Handle hio)
{
H_DEREF(hio, VIo, vio);
return xfile_io_has_completed(&vio->io);
}
// wait until the transfer <hio> completes, and return its buffer.
// output parameters are zeroed on error.
LibError vfs_io_wait(Handle hio, void*& p, size_t& size)
{
H_DEREF(hio, VIo, vio);
return xfile_io_wait(&vio->io, p, size);
}
///////////////////////////////////////////////////////////////////////////////
//
// memory mapping
//
///////////////////////////////////////////////////////////////////////////////
// map the entire (uncompressed!) file <hf> into memory. if currently
// already mapped, return the previous mapping (reference-counted).
// output parameters are zeroed on failure.
//
// the mapping will be removed (if still open) when its file is closed.
// however, map/unmap calls should still be paired so that the mapping
// may be removed when no longer needed.
LibError vfs_map(const Handle hf, const uint UNUSED(flags), void*& p, size_t& size)
{
p = 0;
size = 0;
// need to zero these here in case H_DEREF fails
H_DEREF(hf, VFile, vf);
return xfile_map(&vf->f, p, size);
}
// decrement the reference count for the mapping belonging to file <f>.
// fail if there are no references; remove the mapping if the count reaches 0.
//
// the mapping will be removed (if still open) when its file is closed.
// however, map/unmap calls should still be paired so that the mapping
// may be removed when no longer needed.
LibError vfs_unmap(const Handle hf)
{
H_DEREF(hf, VFile, vf);
return xfile_unmap(&vf->f);
}
//-----------------------------------------------------------------------------
// hotloading
//-----------------------------------------------------------------------------
// called by vfs_reload and vfs_reload_changed_files (which will already
// have rebuilt the VFS - doing so more than once a frame is unnecessary).
static LibError reload_without_rebuild(const char* fn)
{
// invalidate this file's cached blocks to make sure its contents are
// loaded anew.
RETURN_ERR(file_cache_invalidate(fn));
RETURN_ERR(h_reload(fn));
return INFO_OK;
}
// called via console command.
LibError vfs_reload(const char* fn)
{
// if <fn> currently maps to an archive, the VFS must switch
// over to using the loose file (that was presumably changed).
RETURN_ERR(mount_rebuild());
return reload_without_rebuild(fn);
}
// array of reloads requested this frame (see 'do we really need to
// reload' below). go through gyrations to avoid heap allocs.
const size_t MAX_RELOADS_PER_FRAME = 12;
typedef char Path[PATH_MAX];
typedef Path PathList[MAX_RELOADS_PER_FRAME];
// do we really need to reload? try to avoid the considerable cost of
// rebuilding VFS and scanning all Handles.
static bool can_ignore_reload(const char* V_path, PathList pending_reloads, uint num_pending)
{
// note: be careful to avoid 'race conditions' depending on the
// timeframe in which notifications reach us.
// example: editor deletes a.tga; we are notified; reload is
// triggered but fails since the file isn't found; further
// notifications (e.g. renamed a.tmp to a.tga) come within x [ms] and
// are ignored due to a time limit.
// therefore, we can only check for multiple reload requests a frame;
// to that purpose, an array is built and duplicates ignored.
const char* ext = path_extension(V_path);
// .. directory change notification; ignore because we get
// per-file notifications anyway. (note: assume no extension =>
// it's a directory).
if(ext[0] == '\0')
return true;
// .. compiled XML files the engine writes out by the hundreds;
// skipping them is a big performance gain.
if(!stricmp(ext, "xmb"))
return true;
// .. temp files, usually created when an editor saves a file
// (delete, create temp, rename temp); no need to reload those.
if(!stricmp(ext, "tmp"))
return true;
// .. more than one notification for a file; only reload once.
// note: this doesn't suffer from the 'reloaded too early'
// problem described above; if there's more than one
// request in the array, the file has since been written.
for(uint i = 0; i < num_pending; i++)
{
if(!strcmp(pending_reloads[i], V_path))
return true;
}
return false;
}
// get directory change notifications, and reload all affected files.
// must be called regularly (e.g. once a frame). this is much simpler
// than asynchronous notifications: everything would need to be thread-safe.
LibError vfs_reload_changed_files()
{
PathList pending_reloads;
uint num_pending = 0;
// process only as many notifications as we have room for; the others
// will be handled next frame. it's not imagineable that they'll pile up.
while(num_pending < MAX_RELOADS_PER_FRAME)
{
// get next notification
char N_path[PATH_MAX];
LibError ret = dir_get_changed_file(N_path);
if(ret == ERR_AGAIN) // none available; done.
break;
RETURN_ERR(ret);
// convert to VFS path
char P_path[PATH_MAX];
RETURN_ERR(file_make_full_portable_path(N_path, P_path));
char* V_path = pending_reloads[num_pending];
RETURN_ERR(mount_make_vfs_path(P_path, V_path));
if(can_ignore_reload(V_path, pending_reloads, num_pending))
continue;
// path has already been written to pending_reloads,
// so just mark it valid.
num_pending++;
}
// rebuild VFS, in case a file that has been changed is currently
// mounted from an archive (reloading would just grab the unchanged
// version in the archive). the rebuild sees differing mtimes and
// always choses the loose file version. only do this once
// (instead of per reload request) because it's slow (> 1s)!
if(num_pending != 0)
RETURN_ERR(mount_rebuild());
// now actually reload all files in the array we built
for(uint i = 0; i < num_pending; i++)
RETURN_ERR(reload_without_rebuild(pending_reloads[i]));
return INFO_OK;
}
//-----------------------------------------------------------------------------
void vfs_display()
{
tree_display();
}
// allow init more complicated than merely calling mount_init by
// splitting this into a separate function.
static void vfs_init_once(void)
{
mount_init();
}
// make the VFS tree ready for use. must be called before all other
// functions below, barring explicit mentions to the contrary.
//
// rationale: initialization could be done implicitly by calling this
// from all VFS APIs. we refrain from that and require the user to
// call this because a central point of initialization (file_set_root_dir)
// is necessary anyway and this way is simpler/easier to maintain.
void vfs_init()
{
stats_vfs_init_start();
static pthread_once_t once = PTHREAD_ONCE_INIT;
WARN_ERR(pthread_once(&once, vfs_init_once));
stats_vfs_init_finish();
}
void vfs_shutdown()
{
trace_shutdown();
mount_shutdown();
}