forked from 0ad/0ad
712 lines
16 KiB
C++
Executable File
712 lines
16 KiB
C++
Executable File
// virtual file system - transparent access to files in archives;
|
|
// allows multiple search paths
|
|
//
|
|
// Copyright (c) 2003 Jan Wassenberg
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License as
|
|
// published by the Free Software Foundation; either version 2 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// 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. See the GNU
|
|
// General Public License for more details.
|
|
//
|
|
// Contact info:
|
|
// Jan.Wassenberg@stud.uni-karlsruhe.de
|
|
// http://www.stud.uni-karlsruhe.de/~urkt/
|
|
|
|
#include <cstdio>
|
|
#include <cassert>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
#include "lib.h"
|
|
#include "file.h"
|
|
#include "zip.h"
|
|
#include "misc.h"
|
|
#include "vfs.h"
|
|
#include "mem.h"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <stack>
|
|
#include <algorithm>
|
|
|
|
// currently not thread safe, but that will most likely change
|
|
// (if prefetch thread is to be used).
|
|
// not safe to call before main!
|
|
|
|
|
|
// 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).
|
|
|
|
|
|
// rationale for n-archives per PATH entry:
|
|
// We need to be able to unmount specific paths (e.g. when switching mods).
|
|
// Don't want to remount everything (slow), or specify a mod tag when mounting
|
|
// (not this module's job). Instead, we include all archives in one path entry;
|
|
// the game keeps track of what path(s) it mounted for a mod,
|
|
// and unmounts those when needed.
|
|
|
|
struct PATH
|
|
{
|
|
struct PATH* next;
|
|
|
|
// "" if root, otherwise path from root, including DIR_SEP.
|
|
// points to space at end of this struct.
|
|
char* dir;
|
|
|
|
size_t num_archives;
|
|
Handle archives[1];
|
|
|
|
// space allocated here for archive Handles + dir string
|
|
};
|
|
static PATH* path_list;
|
|
|
|
|
|
|
|
|
|
|
|
int vfs_set_root(const char* argv0, const char* root)
|
|
{
|
|
if(access(argv0, X_OK) < 0)
|
|
return errno;
|
|
|
|
char path[PATH_MAX+1];
|
|
path[PATH_MAX] = 0;
|
|
if(!realpath(argv0, path))
|
|
return errno;
|
|
|
|
// remove executable name
|
|
char* fn = strrchr(path, DIR_SEP);
|
|
if(!fn)
|
|
return -1;
|
|
*fn = 0;
|
|
|
|
chdir(path);
|
|
chdir(root);
|
|
|
|
return vfs_mount(".");
|
|
}
|
|
|
|
|
|
int vfs_mount(const char* path)
|
|
{
|
|
size_t i;
|
|
|
|
const size_t path_len = strlen(path);
|
|
if(path_len > VFS_MAX_PATH)
|
|
{
|
|
assert(!"vfs_mount_dir: path name is longer than VFS_MAX_PATH");
|
|
return -1;
|
|
}
|
|
|
|
// security check: path must not contain "..",
|
|
// so that it can't access the FS above the VFS root.
|
|
if(strstr(path, ".."))
|
|
{
|
|
assert(0 && "vfs_mount: .. in path string");
|
|
return -1;
|
|
}
|
|
|
|
// enumerate all archives in <path>
|
|
std::vector<std::string> archives;
|
|
DIR* dir = opendir(path);
|
|
struct dirent* ent;
|
|
while((ent = readdir(dir)))
|
|
{
|
|
const char* fn = ent->d_name;
|
|
struct stat s;
|
|
if(stat(fn, &s) < 0)
|
|
continue;
|
|
// regular file
|
|
if(s.st_mode & S_IFREG)
|
|
{
|
|
char* ext = strrchr(fn, '.');
|
|
// it's a Zip file - add to list
|
|
if(ext && !strcmp(ext, ".zip"))
|
|
archives.push_back(fn);
|
|
}
|
|
}
|
|
closedir(dir);
|
|
|
|
// number of Zip files we'll try to open;
|
|
// final # archives may be less, if opening fails on some of them
|
|
// note: this many are allocated for the PATH entry,
|
|
// but only successfully opened archives are added
|
|
const size_t num_zip_files = archives.size();
|
|
|
|
// alloc search path entry (add to front)
|
|
const size_t archives_size = num_zip_files*sizeof(Handle);
|
|
const size_t dir_size = path_len+1+1; // DIR_SEP and '\0' appended
|
|
const size_t tot_size = sizeof(PATH)+archives_size+dir_size;
|
|
const size_t entry_size = round_up((long)(tot_size), 32);
|
|
PATH* entry = (PATH*)mem_alloc(entry_size, 32);
|
|
if(!entry)
|
|
return ERR_NO_MEM;
|
|
entry->next = path_list;
|
|
path_list = entry;
|
|
|
|
// copy over path string, and convert '/' to platform specific DIR_SEP.
|
|
entry->dir = (char*)&entry->archives[0] + archives_size;
|
|
char* d = entry->dir;
|
|
for(i = 0; i < path_len; i++)
|
|
{
|
|
int c = path[i];
|
|
if(c == '/' || c == '\\' || c == ':')
|
|
{
|
|
assert(c == '/' && "vfs_mount: path string contains platform specific dir separator; use '/'");
|
|
c = DIR_SEP;
|
|
}
|
|
*d++ = c;
|
|
}
|
|
|
|
// add trailing DIR_SEP, so we can just append filename when opening.
|
|
// exception: if mounting the current directory "." (i.e. the VFS root),
|
|
// make it "" - guaranteed to be portable and possibly faster.
|
|
if(!strcmp(path, "."))
|
|
entry->dir[0] = '\0';
|
|
else
|
|
{
|
|
d[0] = DIR_SEP;
|
|
d[1] = '\0';
|
|
}
|
|
|
|
// add archives in alphabetical order
|
|
std::sort(archives.begin(), archives.end());
|
|
Handle* p = entry->archives;
|
|
for(i = 0; i < num_zip_files; i++)
|
|
{
|
|
const Handle h = zip_archive_open(archives[i].c_str());
|
|
if(h > 0)
|
|
*p++ = h;
|
|
}
|
|
entry->num_archives = p - entry->archives; // actually valid archives
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int vfs_umount(const char* path)
|
|
{
|
|
PATH** prev = &path_list;
|
|
PATH* entry = path_list;
|
|
while(entry)
|
|
{
|
|
// found
|
|
if(!strcmp(entry->dir, path))
|
|
{
|
|
// close all archives
|
|
for(size_t i = 0; i < entry->num_archives; i++)
|
|
zip_archive_close(entry->archives[i]);
|
|
|
|
// remove from list
|
|
*prev = entry->next;
|
|
mem_free(entry);
|
|
|
|
return 0;
|
|
}
|
|
|
|
prev = &entry->next;
|
|
entry = entry->next;
|
|
}
|
|
|
|
// not found
|
|
return -1;
|
|
}
|
|
|
|
|
|
typedef int (*VFS_PATH_CB)(const char* full_rel_path, Handle ha, uintptr_t ctx);
|
|
|
|
// call cb, passing the ctx argument, for each mounted path or archive.
|
|
// if it returns an error (< 0) other than ERR_FILE_NOT_FOUND or succeeds
|
|
// (returns 0), return that value; otherwise, continue calling.
|
|
// if it never succeeded, fail with ERR_FILE_NOT_FOUND.
|
|
// rationale: we want to fail with the correct error value if something
|
|
// actually goes wrong (e.g. file locked). callbacks can abort the sequence
|
|
// by returning some error value.
|
|
static int vfs_foreach_path(VFS_PATH_CB cb, const char* fn, uintptr_t ctx)
|
|
{
|
|
char full_rel_path[PATH_MAX+1]; full_rel_path[PATH_MAX] = 0;
|
|
|
|
int err;
|
|
|
|
for(PATH* entry = path_list; entry; entry = entry->next)
|
|
{
|
|
// dir (already includes DIR_SEP)
|
|
snprintf(full_rel_path, PATH_MAX, "%s%s", entry->dir, fn);
|
|
err = cb(full_rel_path, 0, ctx);
|
|
if(err <= 0 && err != ERR_FILE_NOT_FOUND)
|
|
return err;
|
|
|
|
// archive
|
|
for(size_t i = 0; i < entry->num_archives; i++)
|
|
{
|
|
err = cb(fn, entry->archives[i], ctx);
|
|
if(err <= 0 && err != ERR_FILE_NOT_FOUND)
|
|
return err;
|
|
}
|
|
}
|
|
|
|
// if we get here, the function always failed with ERR_FILE_NOT_FOUND or
|
|
// requested the next path.
|
|
return ERR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
|
|
|
|
static int realpath_cb(const char* path, Handle ha, uintptr_t ctx)
|
|
{
|
|
char* full_path = (char*)ctx;
|
|
struct stat s;
|
|
int err;
|
|
|
|
if(!path && !ha)
|
|
{
|
|
assert(0 && "realpath_cb: called with invalid path and archive handle");
|
|
return 1;
|
|
}
|
|
|
|
if(ha)
|
|
{
|
|
err = zip_stat(ha, path, &s);
|
|
if(!err)
|
|
{
|
|
const char* fn = h_filename(ha);
|
|
if(!fn)
|
|
{
|
|
assert(0 && "realpath_cb: h_filename 0, despite successful zip_stat");
|
|
return 1;
|
|
}
|
|
|
|
strncpy(full_path, fn, PATH_MAX);
|
|
return 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
err = stat(path, &s);
|
|
if(!err)
|
|
{
|
|
strncpy(full_path, path, PATH_MAX);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// failed *stat above - return error code.
|
|
return err;
|
|
}
|
|
|
|
int vfs_realpath(const char* fn, char* full_path)
|
|
{
|
|
return vfs_foreach_path(realpath_cb, fn, (uintptr_t)full_path);
|
|
}
|
|
|
|
|
|
static int stat_cb(const char* path, Handle ha, uintptr_t ctx)
|
|
{
|
|
struct stat* s = (struct stat*)ctx;
|
|
|
|
if(!ha)
|
|
return stat(path, s);
|
|
else
|
|
return zip_stat(ha, path, s);
|
|
|
|
assert(0 && "stat_cb: called with invalid path and archive handle");
|
|
return 1;
|
|
}
|
|
|
|
int vfs_stat(const char* fn, struct stat* s)
|
|
{
|
|
return vfs_foreach_path(stat_cb, fn, (uintptr_t)s);
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// file
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
enum
|
|
{
|
|
VF_OPEN = 1,
|
|
VF_ZIP = 2,
|
|
VF_WRITE = 4
|
|
};
|
|
|
|
struct VFile
|
|
{
|
|
int flags;
|
|
size_t size;
|
|
// duplicated in File/ZFile below - oh well.
|
|
// resource data size is fixed anyway.
|
|
|
|
// cached contents of file from vfs_load
|
|
// (can't just use pointer - may be freed behind our back)
|
|
Handle hm;
|
|
|
|
union
|
|
{
|
|
File f;
|
|
ZFile zf;
|
|
};
|
|
};
|
|
|
|
H_TYPE_DEFINE(VFile)
|
|
|
|
|
|
static void VFile_init(VFile* vf, va_list args)
|
|
{
|
|
vf->flags = va_arg(args, int);
|
|
}
|
|
|
|
|
|
static void VFile_dtor(VFile* vf)
|
|
{
|
|
if(vf->flags & VF_OPEN)
|
|
{
|
|
if(vf->flags & VF_ZIP)
|
|
zip_close(&vf->zf);
|
|
else
|
|
file_close(&vf->f);
|
|
|
|
vf->flags &= ~(VF_OPEN);
|
|
}
|
|
|
|
|
|
mem_free_h(vf->hm);
|
|
}
|
|
|
|
|
|
// note: can't use the same callback: must check all paths
|
|
// for a plain file version before looking in archives.
|
|
|
|
|
|
// called for each mounted path or archive.
|
|
static int file_open_cb(const char* path, Handle ha, uintptr_t ctx)
|
|
{
|
|
VFile* vf = (VFile*)ctx;
|
|
|
|
// not a normal file - ask for next path.
|
|
if(ha != 0)
|
|
return 1;
|
|
|
|
int err = file_open(path, vf->flags, &vf->f);
|
|
if(!err)
|
|
{
|
|
vf->size = vf->f.size;
|
|
// somewhat of a hack.. but returning size from file_open
|
|
// is uglier, and relying on start of VFile / File to
|
|
// overlap is unsafe (#define PARANOIA adds a magic field).
|
|
return 0; // found it - done
|
|
}
|
|
|
|
// failed to open.
|
|
return err;
|
|
}
|
|
|
|
|
|
// called for each mounted path or archive.
|
|
static int zip_open_cb(const char* path, Handle ha, uintptr_t ctx)
|
|
{
|
|
VFile* vf = (VFile*)ctx;
|
|
|
|
// file not in archive - ask for next path.
|
|
if(ha == 0)
|
|
return 1;
|
|
|
|
int err = zip_open(ha, path, &vf->zf);
|
|
if(!err)
|
|
{
|
|
vf->size = vf->zf.ucsize;
|
|
vf->flags |= VF_ZIP;
|
|
return 0; // found it - done
|
|
}
|
|
|
|
// failed to open
|
|
return err;
|
|
}
|
|
|
|
|
|
|
|
static int VFile_reload(VFile* vf, const char* fn)
|
|
{
|
|
// 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(vf->flags & VF_OPEN)
|
|
return 0;
|
|
|
|
int err = -1;
|
|
|
|
// careful! this code is a bit tricky. sorry :P
|
|
|
|
// only allow plain-file lookup before looking in Zip archives if
|
|
// opening for writing (we don't support writing to archive), or
|
|
// on dev builds. rationale for disabling in final builds: much faster,
|
|
// and a bit more secure (we can protect archives from modification more
|
|
// easily than the individual files).
|
|
|
|
// dev build: always allow plain-file lookup.
|
|
// final: only allow if opening for writing.
|
|
//
|
|
// note: flags have been set by init already
|
|
#ifdef FINAL
|
|
if(vf->flags & VF_WRITE)
|
|
#endif
|
|
err = vfs_foreach_path(file_open_cb, fn, (uintptr_t)vf);
|
|
|
|
// load from Zip iff we didn't successfully open the plain file,
|
|
// and we're not opening for writing.
|
|
if(err < 0 && !(vf->flags & VF_WRITE))
|
|
err = vfs_foreach_path(zip_open_cb, fn, (uintptr_t)vf);
|
|
|
|
// failed - return error code
|
|
if(err < 0)
|
|
return err;
|
|
|
|
// success
|
|
vf->flags |= VF_OPEN;
|
|
return 0;
|
|
}
|
|
|
|
|
|
Handle vfs_open(const char* fn, int flags /* = 0 */)
|
|
{
|
|
// security check: path must not include ".."
|
|
// (checking start of string isn't enough)
|
|
if(strstr(fn, ".."))
|
|
{
|
|
assert(0 && "vfs_open: .. in path string");
|
|
return 0;
|
|
}
|
|
|
|
return h_alloc(H_VFile, fn, 0, flags);
|
|
// pass file flags to init
|
|
}
|
|
|
|
|
|
inline int vfs_close(Handle& h)
|
|
{
|
|
return h_free(h, H_VFile);
|
|
}
|
|
|
|
|
|
|
|
// we return a ZFile handle for files in an archive, instead of making the
|
|
// handle a member of File, so we don't open 2 handles per file. there's too
|
|
// much internal Zip state to store here - we want to keep it encapsulated.
|
|
// note: since handle types are private to each module, each function has to
|
|
// give the Zip version a crack at its handle, and only continue if it fails.
|
|
// when adding file types, their state should go in File, or else the
|
|
// 'is this our handle' checks would get unwieldy.
|
|
|
|
|
|
|
|
|
|
ssize_t vfs_io(Handle hf, size_t ofs, size_t size, void*& p)
|
|
{
|
|
H_DEREF(hf, VFile, vf);
|
|
|
|
// (vfs_open makes sure it's not opened for writing if zip)
|
|
if(vf->flags & VF_ZIP)
|
|
return zip_read(&vf->zf, ofs, size, p);
|
|
|
|
// normal file:
|
|
// let file_io alloc the buffer if the caller didn't,
|
|
// because it knows about alignment / padding requirements
|
|
return file_io(&vf->f, ofs, size, &p);
|
|
}
|
|
|
|
|
|
Handle vfs_load(const char* fn, void*& p, size_t& size)
|
|
{
|
|
p = 0; // vfs_io needs initial 0 value
|
|
size = 0;
|
|
|
|
Handle hf = vfs_open(fn);
|
|
if(hf <= 0)
|
|
return hf; // error code
|
|
H_DEREF(hf, VFile, vf);
|
|
|
|
Handle hm = 0;
|
|
|
|
// already read into mem - return existing mem handle
|
|
// TODO: what if mapped?
|
|
if(vf->hm > 0)
|
|
{
|
|
p = mem_get_ptr(vf->hm, &size);
|
|
if(p)
|
|
{
|
|
assert(vf->size == size && "vfs_load: mismatch between File and Mem size");
|
|
hm = vf->hm;
|
|
goto skip_read;
|
|
}
|
|
else
|
|
assert(0 && "vfs_load: invalid MEM attached to vfile (0 pointer)");
|
|
// happens if someone frees the pointer. not an error!
|
|
}
|
|
|
|
size = vf->size;
|
|
{ // VC6 goto fix
|
|
ssize_t nread = vfs_io(hf, 0, size, p);
|
|
if(nread > 0)
|
|
hm = mem_assign(p, size);
|
|
}
|
|
|
|
skip_read:
|
|
|
|
vfs_close(hf);
|
|
|
|
// if we fail, make sure these are set to 0
|
|
// (they may have been assigned values above)
|
|
if(hm <= 0)
|
|
p = 0, size = 0;
|
|
|
|
return hm;
|
|
}
|
|
|
|
|
|
int vfs_store(const char* fn, void* p, size_t size)
|
|
{
|
|
Handle hf = vfs_open(fn, VFS_WRITE);
|
|
if(hf <= 0)
|
|
return (int)hf; // error code
|
|
H_DEREF(hf, VFile, vf);
|
|
int ret = vfs_io(hf, 0, size, p);
|
|
vfs_close(hf);
|
|
return ret;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// separate mem and mmap handles
|
|
|
|
//mmap used rarely, don't need transparency
|
|
//mmap needs filename (so it's invalidated when file changes), passed to h_alloc
|
|
//also uses reload and dtor - don't want to stretch mem too far (doesn't belong there)
|
|
|
|
|
|
struct MMap
|
|
{
|
|
void* p;
|
|
size_t size;
|
|
File f;
|
|
};
|
|
|
|
H_TYPE_DEFINE(MMap)
|
|
|
|
|
|
static void MMap_init(MMap* m, va_list args)
|
|
{
|
|
}
|
|
|
|
|
|
void MMap_dtor(MMap* m)
|
|
{
|
|
// munmap(m->p, m->size);
|
|
// vfs_close(m->hf);
|
|
}
|
|
|
|
/*
|
|
|
|
// get pointer to archive in memory
|
|
Handle ham;
|
|
if(0)
|
|
// if(zip_archive_info(0, 0, &ham) == 0)
|
|
{
|
|
void* archive_p;
|
|
size_t archive_size;
|
|
archive_p = mem_get_ptr(ham, &archive_size);
|
|
|
|
// return file's pos in mapping
|
|
assert(ofs < archive_size && "vfs_load: vfile.ofs exceeds Zip archive size");
|
|
_p = (char*)archive_p + ofs;
|
|
_size = out_size;
|
|
hm = mem_assign(_p, _size);
|
|
goto done;
|
|
}
|
|
}
|
|
*/
|
|
|
|
int MMap_reload(MMap* m, const char* fn)
|
|
{
|
|
/*
|
|
Handle hf = vfs_open(fn);
|
|
if(!hf)
|
|
return -1;
|
|
File* vf = H_USER_DATA(hf, File);
|
|
Handle hf2;
|
|
size_t ofs;
|
|
#if 0
|
|
if(vf->hz)
|
|
if(zip_get_file(vf->hz, hf2, ofs) < 0)
|
|
return -1;
|
|
#endif
|
|
void* p = mmap(0, (uint)vf->size, PROT_READ, MAP_PRIVATE, vf->fd, 0);
|
|
if(!p)
|
|
{
|
|
vfs_close(hf);
|
|
return -1;
|
|
}
|
|
|
|
m->p = p;
|
|
m->size = vf->size;
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
|
|
Handle vfs_map(const char* fn, int flags, void*& p, size_t& size)
|
|
{
|
|
Handle hf = vfs_open(fn, flags);
|
|
H_DEREF(hf, VFile, vf);
|
|
int err = file_map(&vf->f, p, size);
|
|
if(err < 0)
|
|
return err;
|
|
MEM_DTOR dtor = 0;
|
|
uintptr_t ctx = 0;
|
|
return mem_assign(p, size, 0, dtor, ctx);
|
|
}
|
|
|
|
|
|
int vfs_unmap(Handle& hm)
|
|
{
|
|
return h_free(hm, H_MMap);
|
|
}
|
|
|