/** * PANDA 3D SOFTWARE * Copyright (c) Carnegie Mellon University. All rights reserved. * * All use of this software is subject to the terms of the revised BSD * license. You should have received a copy of this license along * with this source code in a file named "LICENSE." * * @file virtualFileSystem.cxx * @author drose * @date 2002-08-03 */ #include "virtualFileSystem.h" #include "virtualFileSimple.h" #include "virtualFileComposite.h" #include "virtualFileMount.h" #include "virtualFileMountMultifile.h" #include "virtualFileMountRamdisk.h" #include "virtualFileMountSystem.h" #include "streamWrapper.h" #include "dSearchPath.h" #include "dcast.h" #include "config_express.h" #include "executionEnvironment.h" #include "pset.h" VirtualFileSystem *VirtualFileSystem::_global_ptr = NULL; /** * */ VirtualFileSystem:: VirtualFileSystem() : vfs_case_sensitive ("vfs-case-sensitive", #ifdef NDEBUG false, // The default for a production build is not case-sensitive; // this avoids runtime overhead to verify case sensitivity. #else true, #endif PRC_DESC("Set this true to make the VirtualFileSystem present the native " "OS-provided filesystem as if it were a case-sensitive file " "system, even if it is not (e.g. on Windows). This variable " "has no effect if the native filesystem is already case-sensitive, " "and it has no effect on mounted multifile systems, which are " "always case-sensitive.")), vfs_implicit_pz ("vfs-implicit-pz", true, PRC_DESC("When this is true, the VirtualFileSystem will pretend a named " "file exists even if it doesn't, as long as a filename with the " "same name and the additional extension .pz does exist. In this " "case, the VirtualFileSystem will implicitly open the .pz file " "and decompress it on-the-fly.")), vfs_implicit_mf ("vfs-implicit-mf", false, PRC_DESC("When this is true, the VirtualFileSystem will automatically " "mount multifiles on-the-fly when they are used as directories. " "For instance, opening the file /c/files/foo.mf/dirname/mytex.jpg " "will implicitly retrieve a file named 'dirname/mytex.jpg' " "within the multifile /c/files/foo.mf, even if the multifile " "has not already been mounted. This makes all of your multifiles " "act like directories.")) { _cwd = "/"; _mount_seq = 0; } /** * */ VirtualFileSystem:: ~VirtualFileSystem() { unmount_all(); } /** * Mounts the indicated Multifile at the given mount point. */ bool VirtualFileSystem:: mount(Multifile *multifile, const Filename &mount_point, int flags) { PT(VirtualFileMountMultifile) new_mount = new VirtualFileMountMultifile(multifile); return mount(new_mount, mount_point, flags); } /** * Mounts the indicated system file or directory at the given mount point. If * the named file is a directory, mounts the directory. If the named file is * a Multifile, mounts it as a Multifile. Returns true on success, false on * failure. * * A given system directory may be mounted to multiple different mount point, * and the same mount point may share multiple system directories. In the * case of ambiguities (that is, two different files with exactly the same * full pathname), the most-recently mounted system wins. * * The filename specified as the first parameter must refer to a real, * physical filename on disk; it cannot be a virtual file already appearing * within the vfs filespace. However, it is possible to mount such a file; * see mount_loop() for this. * * Note that a mounted VirtualFileSystem directory is fully case-sensitive, * unlike the native Windows file system, so you must refer to files within * the virtual file system with exactly the right case. */ bool VirtualFileSystem:: mount(const Filename &physical_filename, const Filename &mount_point, int flags, const string &password) { if (!physical_filename.exists()) { express_cat->warning() << "Attempt to mount " << physical_filename << ", not found.\n"; return false; } if (physical_filename.is_directory()) { PT(VirtualFileMountSystem) new_mount = new VirtualFileMountSystem(physical_filename); return mount(new_mount, mount_point, flags); } else { // It's not a directory; it must be a Multifile. PT(Multifile) multifile = new Multifile; multifile->set_encryption_password(password); // For now these are always opened read only. Maybe later we'll support // read-write on Multifiles. flags |= MF_read_only; if (!multifile->open_read(physical_filename)) { return false; } return mount(multifile, mount_point, flags); } } /** * This is similar to mount(), but it receives the name of a Multifile that * already appears within the virtual file system. It can be used to mount a * Multifile that is itself hosted within a virtually-mounted Multifile. * * This interface can also be used to mount physical files (that appear within * the virtual filespace), but it cannot be used to mount directories. Use * mount() if you need to mount a directory. * * Note that there is additional overhead, in the form of additional buffer * copies of the data, for recursively mounting a multifile like this. */ bool VirtualFileSystem:: mount_loop(const Filename &virtual_filename, const Filename &mount_point, int flags, const string &password) { PT(VirtualFile) file = get_file(virtual_filename, false); if (file == NULL) { express_cat->warning() << "Attempt to mount " << virtual_filename << ", not found.\n"; return false; } if (file->is_directory()) { PT(VirtualFileMountSystem) new_mount = new VirtualFileMountSystem(virtual_filename); return mount(new_mount, mount_point, flags); } else { // It's not a directory; it must be a Multifile. PT(Multifile) multifile = new Multifile; multifile->set_encryption_password(password); // For now these are always opened read only. Maybe later we'll support // read-write on Multifiles. flags |= MF_read_only; if (!multifile->open_read(virtual_filename)) { return false; } return mount(multifile, mount_point, flags); } } /** * Adds the given VirtualFileMount object to the mount list. This is a lower- * level function that the other flavors of mount(); it requires you to create * a VirtualFileMount object specifically. */ bool VirtualFileSystem:: mount(VirtualFileMount *mount, const Filename &mount_point, int flags) { if (express_cat->is_debug()) { express_cat->debug() << "mount " << *mount << " under " << mount_point << "\n"; } _lock.acquire(); bool result = do_mount(mount, mount_point, flags); _lock.release(); return result; } /** * Unmounts all appearances of the indicated Multifile from the file system. * Returns the number of appearances unmounted. */ int VirtualFileSystem:: unmount(Multifile *multifile) { _lock.acquire(); Mounts::iterator ri, wi; wi = ri = _mounts.begin(); while (ri != _mounts.end()) { VirtualFileMount *mount = (*ri); (*wi) = mount; if (mount->is_exact_type(VirtualFileMountMultifile::get_class_type())) { VirtualFileMountMultifile *mmount = DCAST(VirtualFileMountMultifile, mount); if (mmount->get_multifile() == multifile) { // Remove this one. Don't increment wi. if (express_cat->is_debug()) { express_cat->debug() << "unmount " << *mount << " from " << mount->get_mount_point() << "\n"; } mount->_file_system = NULL; } else { // Don't remove this one. ++wi; } } else { // Don't remove this one. ++wi; } ++ri; } int num_removed = _mounts.end() - wi; _mounts.erase(wi, _mounts.end()); ++_mount_seq; _lock.release(); return num_removed; } /** * Unmounts all appearances of the indicated directory name or multifile name * from the file system. Returns the number of appearances unmounted. */ int VirtualFileSystem:: unmount(const Filename &physical_filename) { _lock.acquire(); Mounts::iterator ri, wi; wi = ri = _mounts.begin(); while (ri != _mounts.end()) { VirtualFileMount *mount = (*ri); (*wi) = mount; if (mount->is_exact_type(VirtualFileMountSystem::get_class_type())) { VirtualFileMountSystem *smount = DCAST(VirtualFileMountSystem, mount); if (smount->get_physical_filename() == physical_filename) { // Remove this one. Don't increment wi. if (express_cat->is_debug()) { express_cat->debug() << "unmount " << *mount << " from " << mount->get_mount_point() << "\n"; } mount->_file_system = NULL; } else { // Don't remove this one. ++wi; } } else if (mount->is_exact_type(VirtualFileMountMultifile::get_class_type())) { VirtualFileMountMultifile *mmount = DCAST(VirtualFileMountMultifile, mount); if (mmount->get_multifile()->get_multifile_name() == physical_filename) { // Remove this one. Don't increment wi. if (express_cat->is_debug()) { express_cat->debug() << "unmount " << *mount << " from " << mount->get_mount_point() << "\n"; } mount->_file_system = NULL; } else { // Don't remove this one. ++wi; } } else { // Don't remove this one. ++wi; } ++ri; } int num_removed = _mounts.end() - wi; _mounts.erase(wi, _mounts.end()); ++_mount_seq; _lock.release(); return num_removed; } /** * Unmounts the indicated VirtualFileMount object from the file system. * Returns the number of appearances unmounted. */ int VirtualFileSystem:: unmount(VirtualFileMount *mount) { _lock.acquire(); Mounts::iterator ri, wi; wi = ri = _mounts.begin(); while (ri != _mounts.end()) { (*wi) = (*ri); if ((*ri) == mount) { // Remove this one. Don't increment wi. if (express_cat->is_debug()) { express_cat->debug() << "unmount " << *mount << " from " << mount->get_mount_point() << "\n"; } (*ri)->_file_system = NULL; } else { // Don't remove this one. ++wi; } ++ri; } int num_removed = _mounts.end() - wi; _mounts.erase(wi, _mounts.end()); ++_mount_seq; _lock.release(); return num_removed; } /** * Unmounts all systems attached to the given mount point from the file * system. Returns the number of appearances unmounted. */ int VirtualFileSystem:: unmount_point(const Filename &mount_point) { _lock.acquire(); Filename nmp = normalize_mount_point(mount_point); Mounts::iterator ri, wi; wi = ri = _mounts.begin(); while (ri != _mounts.end()) { VirtualFileMount *mount = (*ri); (*wi) = mount; if (mount->get_mount_point() == nmp) { // Remove this one. Don't increment wi. if (express_cat->is_debug()) { express_cat->debug() << "unmount " << *mount << " from " << mount->get_mount_point() << "\n"; } mount->_file_system = NULL; } else { // Don't remove this one. ++wi; } ++ri; } int num_removed = _mounts.end() - wi; _mounts.erase(wi, _mounts.end()); ++_mount_seq; _lock.release(); return num_removed; } /** * Unmounts all files from the file system. Returns the number of systems * unmounted. */ int VirtualFileSystem:: unmount_all() { _lock.acquire(); Mounts::iterator ri; for (ri = _mounts.begin(); ri != _mounts.end(); ++ri) { VirtualFileMount *mount = (*ri); if (express_cat->is_debug()) { express_cat->debug() << "unmount " << *mount << " from " << mount->get_mount_point() << "\n"; } mount->_file_system = NULL; } int num_removed = _mounts.size(); _mounts.clear(); ++_mount_seq; _lock.release(); return num_removed; } /** * Returns the number of individual mounts in the system. */ int VirtualFileSystem:: get_num_mounts() const { ((VirtualFileSystem *)this)->_lock.acquire(); int result = _mounts.size(); ((VirtualFileSystem *)this)->_lock.release(); return result; } /** * Returns the nth mount in the system. */ PT(VirtualFileMount) VirtualFileSystem:: get_mount(int n) const { ((VirtualFileSystem *)this)->_lock.acquire(); nassertd(n >= 0 && n < (int)_mounts.size()) { ((VirtualFileSystem *)this)->_lock.release(); return NULL; } PT(VirtualFileMount) result = _mounts[n]; ((VirtualFileSystem *)this)->_lock.release(); return result; } /** * Changes the current directory. This is used to resolve relative pathnames * in get_file() and/or find_file(). Returns true if successful, false * otherwise. */ bool VirtualFileSystem:: chdir(const Filename &new_directory) { _lock.acquire(); if (new_directory == "/") { // We can always return to the root. _cwd = new_directory; _lock.release(); return true; } PT(VirtualFile) file = do_get_file(new_directory, OF_status_only); if (file != (VirtualFile *)NULL && file->is_directory()) { _cwd = file->get_filename(); _lock.release(); return true; } _lock.release(); return false; } /** * Returns the current directory name. See chdir(). */ Filename VirtualFileSystem:: get_cwd() const { ((VirtualFileSystem *)this)->_lock.acquire(); Filename result = _cwd; ((VirtualFileSystem *)this)->_lock.release(); return result; } /** * Attempts to create a directory within the file system. Returns true on * success, false on failure (for instance, because the parent directory does * not exist, or is read-only). If the directory already existed prior to * this call, returns true. */ bool VirtualFileSystem:: make_directory(const Filename &filename) { _lock.acquire(); PT(VirtualFile) result = do_get_file(filename, OF_make_directory); _lock.release(); nassertr_always(result != NULL, false); return result->is_directory(); } /** * Attempts to create a directory within the file system. Will also create * any intervening directories needed. Returns true on success, false on * failure. */ bool VirtualFileSystem:: make_directory_full(const Filename &filename) { _lock.acquire(); // First, make sure everything up to the last path is known. We don't care // too much if any of these fail; maybe they failed because the directory // was already there. string dirname = filename; size_t slash = dirname.find('/', 1); while (slash != string::npos) { Filename component(dirname.substr(0, slash)); do_get_file(component, OF_make_directory); slash = dirname.find('/', slash + 1); } // Now make the last one, and check the return value. PT(VirtualFile) result = do_get_file(filename, OF_make_directory); _lock.release(); nassertr_always(result != NULL, false); return result->is_directory(); } /** * Looks up the file by the indicated name in the file system. Returns a * VirtualFile pointer representing the file if it is found, or NULL if it is * not. * * If status_only is true, the file will be checked for existence and length * and so on, but the returned file's contents cannot be read. This is an * optimization which is especially important for certain mount types, for * instance HTTP, for which opening a file to determine its status is * substantially less expensive than opening it to read its contents. */ PT(VirtualFile) VirtualFileSystem:: get_file(const Filename &filename, bool status_only) const { int open_flags = status_only ? OF_status_only : 0; ((VirtualFileSystem *)this)->_lock.acquire(); PT(VirtualFile) result = do_get_file(filename, open_flags); ((VirtualFileSystem *)this)->_lock.release(); return result; } /** * Attempts to create a file by the indicated name in the filesystem, if * possible, and returns it. If a file by this name already exists, returns * the same thing as get_file(). If the filename is located within a read- * only directory, or the directory doesn't exist, returns NULL. */ PT(VirtualFile) VirtualFileSystem:: create_file(const Filename &filename) { ((VirtualFileSystem *)this)->_lock.acquire(); PT(VirtualFile) result = do_get_file(filename, OF_create_file); ((VirtualFileSystem *)this)->_lock.release(); return result; } /** * Uses the indicated search path to find the file within the file system. * Returns the first occurrence of the file found, or NULL if the file cannot * be found. */ PT(VirtualFile) VirtualFileSystem:: find_file(const Filename &filename, const DSearchPath &searchpath, bool status_only) const { if (!filename.is_local()) { return get_file(filename, status_only); } int num_directories = searchpath.get_num_directories(); for (int i = 0; i < num_directories; ++i) { Filename match(searchpath.get_directory(i), filename); if (searchpath.get_directory(i) == "." && filename.is_fully_qualified()) { // A special case for the "." directory: to avoid prefixing an endless // stream of . in front of files, if the filename already has a . // prefixed (i.e. is_fully_qualified() is true), we don't prefix // another one. match = filename; } PT(VirtualFile) found_file = get_file(match, status_only); if (found_file != (VirtualFile *)NULL) { return found_file; } } return NULL; } /** * Attempts to delete the indicated file or directory. This can remove a * single file or an empty directory. It will not remove a nonempty * directory. Returns true on success, false on failure. */ bool VirtualFileSystem:: delete_file(const Filename &filename) { PT(VirtualFile) file = get_file(filename, true); if (file == (VirtualFile *)NULL) { return false; } return file->delete_file(); } /** * Attempts to move or rename the indicated file or directory. If the * original file is an ordinary file, it will quietly replace any already- * existing file in the new filename (but not a directory). If the original * file is a directory, the new filename must not already exist. * * If the file is a directory, the new filename must be within the same mount * point. If the file is an ordinary file, the new filename may be anywhere; * but if it is not within the same mount point then the rename operation is * automatically performed as a two-step copy-and-delete operation. */ bool VirtualFileSystem:: rename_file(const Filename &orig_filename, const Filename &new_filename) { _lock.acquire(); PT(VirtualFile) orig_file = do_get_file(orig_filename, OF_status_only); if (orig_file == (VirtualFile *)NULL) { _lock.release(); return false; } PT(VirtualFile) new_file = do_get_file(new_filename, OF_status_only | OF_allow_nonexist); if (new_file == (VirtualFile *)NULL) { _lock.release(); return false; } _lock.release(); return orig_file->rename_file(new_file); } /** * Attempts to copy the contents of the indicated file to the indicated file. * Returns true on success, false on failure. */ bool VirtualFileSystem:: copy_file(const Filename &orig_filename, const Filename &new_filename) { PT(VirtualFile) orig_file = get_file(orig_filename, true); if (orig_file == (VirtualFile *)NULL) { return false; } PT(VirtualFile) new_file = create_file(new_filename); if (new_file == (VirtualFile *)NULL) { return false; } return orig_file->copy_file(new_file); } /** * Searches the given search path for the filename. If it is found, updates * the filename to the full pathname found and returns true; otherwise, * returns false. */ bool VirtualFileSystem:: resolve_filename(Filename &filename, const DSearchPath &searchpath, const string &default_extension) const { PT(VirtualFile) found; if (filename.is_local()) { found = find_file(filename, searchpath, true); if (found.is_null()) { // We didn't find it with the given extension; can we try the default // extension? if (filename.get_extension().empty() && !default_extension.empty()) { Filename try_ext = filename; try_ext.set_extension(default_extension); found = find_file(try_ext, searchpath, true); } } } else { if (exists(filename)) { // The full pathname exists. Return true. return true; } else { // The full pathname doesn't exist with the given extension; does it // exist with the default extension? if (filename.get_extension().empty() && !default_extension.empty()) { Filename try_ext = filename; try_ext.set_extension(default_extension); found = get_file(try_ext, true); } } } if (!found.is_null()) { filename = found->get_original_filename(); return true; } return false; } /** * Searches all the directories in the search list for the indicated file, in * order. Fills up the results list with *all* of the matching filenames * found, if any. Returns the number of matches found. * * It is the responsibility of the the caller to clear the results list first; * otherwise, the newly-found files will be appended to the list. */ int VirtualFileSystem:: find_all_files(const Filename &filename, const DSearchPath &searchpath, DSearchPath::Results &results) const { int num_added = 0; if (filename.is_local()) { int num_directories = searchpath.get_num_directories(); for (int i = 0; i < num_directories; ++i) { Filename match(searchpath.get_directory(i), filename); if (exists(match)) { if (searchpath.get_directory(i) == "." && filename.is_fully_qualified()) { // A special case for the "." directory: to avoid prefixing an // endless stream of . in front of files, if the filename already // has a . prefixed (i.e. is_fully_fully_qualified() is true), we // don't prefix another one. results.add_file(filename); } else { results.add_file(match); } ++num_added; } } } return num_added; } /** * Print debugging information. (e.g. from Python or gdb prompt). */ void VirtualFileSystem:: write(ostream &out) const { ((VirtualFileSystem *)this)->_lock.acquire(); Mounts::const_iterator mi; for (mi = _mounts.begin(); mi != _mounts.end(); ++mi) { VirtualFileMount *mount = (*mi); mount->write(out); } ((VirtualFileSystem *)this)->_lock.release(); } /** * Returns the default global VirtualFileSystem. You may create your own * personal VirtualFileSystem objects and use them for whatever you like, but * Panda will attempt to load models and stuff from this default object. * * Initially, the global VirtualFileSystem is set up to mount the OS * filesystem to root; i.e. it is equivalent to the OS filesystem. This may * be subsequently adjusted by the user. */ VirtualFileSystem *VirtualFileSystem:: get_global_ptr() { if (_global_ptr == (VirtualFileSystem *)NULL) { // Make sure this is initialized. init_libexpress(); _global_ptr = new VirtualFileSystem; // Set up the default mounts. First, there is always the root mount. _global_ptr->mount("/", "/", 0); // And our initial cwd comes from the environment. _global_ptr->chdir(ExecutionEnvironment::get_cwd()); // Then, we add whatever mounts are listed in the Configrc file. ConfigVariableList mounts ("vfs-mount", PRC_DESC("vfs-mount system-filename mount-point [options]")); int num_unique_values = mounts.get_num_unique_values(); for (int i = 0; i < num_unique_values; i++) { string mount_desc = mounts.get_unique_value(i); // The vfs-mount syntax is: // vfs-mount system-filename mount-point [options] // The last two spaces mark the beginning of the mount point, and of the // options, respectively. There might be multiple spaces in the system // filename, which are part of the filename. // The last space marks the beginning of the mount point. Spaces before // that are part of the system filename. size_t space = mount_desc.rfind(' '); if (space == string::npos) { express_cat.warning() << "No space in vfs-mount descriptor: " << mount_desc << "\n"; } else { string mount_point = mount_desc.substr(space + 1); while (space > 0 && isspace(mount_desc[space - 1])) { space--; } mount_desc = mount_desc.substr(0, space); string options; space = mount_desc.rfind(' '); if (space != string::npos) { // If there's another space, we have the optional options field. options = mount_point; mount_point = mount_desc.substr(space + 1); while (space > 0 && isspace(mount_desc[space - 1])) { --space; } mount_desc = mount_desc.substr(0, space); } mount_desc = ExecutionEnvironment::expand_string(mount_desc); Filename physical_filename = Filename::from_os_specific(mount_desc); int flags; string password; parse_options(options, flags, password); _global_ptr->mount(physical_filename, mount_point, flags, password); } } ConfigVariableString vfs_mount_ramdisk ("vfs-mount-ramdisk", "", PRC_DESC("vfs-mount-ramdisk mount-point [options]")); if (!vfs_mount_ramdisk.empty()) { string mount_point = vfs_mount_ramdisk; string options; size_t space = mount_point.rfind(' '); if (space != string::npos) { // If there's a space, we have the optional options field. options = mount_point.substr(space + 1); while (space > 0 && isspace(mount_point[space - 1])) { --space; } mount_point = mount_point.substr(0, space); } int flags; string password; parse_options(options, flags, password); PT(VirtualFileMount) ramdisk = new VirtualFileMountRamdisk; _global_ptr->mount(ramdisk, mount_point, flags); } } return _global_ptr; } /** * Convenience function; returns a newly allocated istream if the file exists * and can be read, or NULL otherwise. Does not return an invalid istream. * * If auto_unwrap is true, an explicitly-named .pz file is automatically * decompressed and the decompressed contents are returned. This is different * than vfs-implicit-pz, which will automatically decompress a file if the * extension .pz is *not* given. */ istream *VirtualFileSystem:: open_read_file(const Filename &filename, bool auto_unwrap) const { PT(VirtualFile) file = get_file(filename, false); if (file == (VirtualFile *)NULL) { return NULL; } istream *str = file->open_read_file(auto_unwrap); if (str != (istream *)NULL && str->fail()) { close_read_file(str); str = (istream *)NULL; } return str; } /** * Closes a file opened by a previous call to open_read_file(). This really * just deletes the istream pointer, but it is recommended to use this * interface instead of deleting it explicitly, to help work around compiler * issues. */ void VirtualFileSystem:: close_read_file(istream *stream) { if (stream != (istream *)NULL) { // For some reason--compiler bug in gcc 3.2?--explicitly deleting the // stream pointer does not call the appropriate global delete function; // instead apparently calling the system delete function. So we call the // delete function by hand instead. #if (!defined(WIN32_VC) && !defined(WIN64_VC)) && !defined(USE_MEMORY_NOWRAPPERS) && defined(REDEFINE_GLOBAL_OPERATOR_NEW) stream->~istream(); (*global_operator_delete)(stream); #else delete stream; #endif } } /** * Convenience function; returns a newly allocated ostream if the file exists * and can be written, or NULL otherwise. Does not return an invalid ostream. * * If auto_wrap is true, an explicitly-named .pz file is automatically * compressed while writing. If truncate is true, the file is truncated to * zero length before writing. */ ostream *VirtualFileSystem:: open_write_file(const Filename &filename, bool auto_wrap, bool truncate) { PT(VirtualFile) file = create_file(filename); if (file == (VirtualFile *)NULL) { return NULL; } ostream *str = file->open_write_file(auto_wrap, truncate); if (str != (ostream *)NULL && str->fail()) { close_write_file(str); str = (ostream *)NULL; } return str; } /** * Works like open_write_file(), but the file is opened in append mode. Like * open_write_file, the returned pointer should eventually be passed to * close_write_file(). */ ostream *VirtualFileSystem:: open_append_file(const Filename &filename) { PT(VirtualFile) file = create_file(filename); if (file == (VirtualFile *)NULL) { return NULL; } ostream *str = file->open_append_file(); if (str != (ostream *)NULL && str->fail()) { close_write_file(str); str = (ostream *)NULL; } return str; } /** * Closes a file opened by a previous call to open_write_file(). This really * just deletes the ostream pointer, but it is recommended to use this * interface instead of deleting it explicitly, to help work around compiler * issues. */ void VirtualFileSystem:: close_write_file(ostream *stream) { if (stream != (ostream *)NULL) { #if (!defined(WIN32_VC) && !defined(WIN64_VC)) && !defined(USE_MEMORY_NOWRAPPERS) && defined(REDEFINE_GLOBAL_OPERATOR_NEW) stream->~ostream(); (*global_operator_delete)(stream); #else delete stream; #endif } } /** * Convenience function; returns a newly allocated iostream if the file exists * and can be written, or NULL otherwise. Does not return an invalid * iostream. */ iostream *VirtualFileSystem:: open_read_write_file(const Filename &filename, bool truncate) { PT(VirtualFile) file = create_file(filename); if (file == (VirtualFile *)NULL) { return NULL; } iostream *str = file->open_read_write_file(truncate); if (str != (iostream *)NULL && str->fail()) { close_read_write_file(str); str = (iostream *)NULL; } return str; } /** * Works like open_read_write_file(), but the file is opened in append mode. * Like open_read_write_file, the returned pointer should eventually be passed * to close_read_write_file(). */ iostream *VirtualFileSystem:: open_read_append_file(const Filename &filename) { PT(VirtualFile) file = create_file(filename); if (file == (VirtualFile *)NULL) { return NULL; } iostream *str = file->open_read_append_file(); if (str != (iostream *)NULL && str->fail()) { close_read_write_file(str); str = (iostream *)NULL; } return str; } /** * Closes a file opened by a previous call to open_read_write_file(). This * really just deletes the iostream pointer, but it is recommended to use this * interface instead of deleting it explicitly, to help work around compiler * issues. */ void VirtualFileSystem:: close_read_write_file(iostream *stream) { if (stream != (iostream *)NULL) { #if (!defined(WIN32_VC) && !defined(WIN64_VC)) && !defined(USE_MEMORY_NOWRAPPERS) && defined(REDEFINE_GLOBAL_OPERATOR_NEW) stream->~iostream(); (*global_operator_delete)(stream); #else delete stream; #endif } } /** * See Filename::atomic_compare_and_exchange_contents(). */ bool VirtualFileSystem:: atomic_compare_and_exchange_contents(const Filename &filename, string &orig_contents, const string &old_contents, const string &new_contents) { PT(VirtualFile) file = create_file(filename); if (file == NULL) { return false; } return file->atomic_compare_and_exchange_contents(orig_contents, old_contents, new_contents); } /** * See Filename::atomic_read_contents(). */ bool VirtualFileSystem:: atomic_read_contents(const Filename &filename, string &contents) const { PT(VirtualFile) file = get_file(filename, false); if (file == NULL) { return false; } return file->atomic_read_contents(contents); } /** * Adds to names a list of all the mount points in use that are one directory * below path, if any. That is, these are the external files or directories * mounted directly to the indicated path. * * The names vector is filled with a set of basenames, the basename part of * the mount point. */ void VirtualFileSystem:: scan_mount_points(vector_string &names, const Filename &path) const { nassertv(!path.empty() && !path.is_local()); string prefix = path.get_fullpath().substr(1); Mounts::const_iterator mi; for (mi = _mounts.begin(); mi != _mounts.end(); ++mi) { VirtualFileMount *mount = (*mi); string mount_point = mount->get_mount_point(); if (prefix.empty()) { // The indicated path is the root. Is the mount point on the root? if (mount_point.find('/') == string::npos) { // No embedded slashes, so the mount point is only one directory below // the root. names.push_back(mount_point); } } else { if (mount_point.substr(0, prefix.length()) == prefix && mount_point.length() > prefix.length() && mount_point[prefix.length()] == '/') { // This mount point is below the indicated path. Is it only one // directory below? string basename = mount_point.substr(prefix.length()); if (basename.find('/') == string::npos) { // No embedded slashes, so it's only one directory below. names.push_back(basename); } } } } } /** * Parses all of the option flags in the options list on the vfs-mount * Config.prc line. */ void VirtualFileSystem:: parse_options(const string &options, int &flags, string &password) { flags = 0; password = string(); // Split the options up by commas. size_t p = 0; size_t q = options.find(',', p); while (q != string::npos) { parse_option(options.substr(p, q - p), flags, password); p = q + 1; q = options.find(',', p); } parse_option(options.substr(p), flags, password); } /** * Parses one of the option flags in the options list on the vfs-mount * Config.prc line. */ void VirtualFileSystem:: parse_option(const string &option, int &flags, string &password) { if (option == "0" || option.empty()) { // 0 is the null option. } else if (option == "ro") { flags |= MF_read_only; } else if (option.substr(0, 3) == "pw:") { password = option.substr(3); } else { express_cat.warning() << "Invalid option on vfs-mount: \"" << option << "\"\n"; } } /** * Converts the mount point string supplied by the user to standard form * (relative to the current directory, with no double slashes, and not * terminating with a slash). The initial slash is removed. * * Assumes the lock is already held. */ Filename VirtualFileSystem:: normalize_mount_point(const Filename &mount_point) const { Filename nmp = mount_point; if (nmp.is_local()) { nmp = Filename(_cwd, mount_point); } nmp.standardize(); nassertr(!nmp.empty() && nmp[0] == '/', nmp); return nmp.get_fullpath().substr(1); } /** * The private implementation of mount(). Assumes the lock is already held. */ bool VirtualFileSystem:: do_mount(VirtualFileMount *mount, const Filename &mount_point, int flags) { nassertr(mount->_file_system == NULL, false); mount->_file_system = this; mount->_mount_point = normalize_mount_point(mount_point); mount->_mount_flags = flags; _mounts.push_back(mount); ++_mount_seq; return true; } /** * The private implementation of get_file(), create_file(), and * make_directory(). Assumes the lock is already held. */ PT(VirtualFile) VirtualFileSystem:: do_get_file(const Filename &filename, int open_flags) const { if (filename.empty()) { return NULL; } Filename pathname(filename); if (pathname.is_local()) { pathname = Filename(_cwd, filename); if (filename.is_text()) { pathname.set_text(); } } pathname.standardize(); Filename strpath = pathname.get_filename_index(0).get_fullpath().substr(1); strpath.set_type(filename.get_type()); // Also transparently look for a regular file suffixed .pz. Filename strpath_pz = strpath + ".pz"; // Now scan all the mount points, from the back (since later mounts override // more recent ones), until a match is found. PT(VirtualFile) found_file = NULL; VirtualFileComposite *composite_file = NULL; // We use an index instead of an iterator, since the vector might change if // implicit mounts are added during this loop. unsigned int start_seq = _mount_seq; size_t i = _mounts.size(); while (i > 0) { --i; VirtualFileMount *mount = _mounts[i]; Filename mount_point = mount->get_mount_point(); if (strpath == mount_point) { // Here's an exact match on the mount point. This filename is the root // directory of this mount object. if (consider_match(found_file, composite_file, mount, "", pathname, false, open_flags)) { return found_file; } } else if (mount_point.empty()) { // This is the root mount point; all files are in here. if (consider_match(found_file, composite_file, mount, strpath, pathname, false, open_flags)) { return found_file; } #ifdef HAVE_ZLIB if (vfs_implicit_pz) { if (consider_match(found_file, composite_file, mount, strpath_pz, pathname, true, open_flags)) { return found_file; } } #endif // HAVE_ZLIB } else if (strpath.length() > mount_point.length() && mount_point == strpath.substr(0, mount_point.length()) && strpath[mount_point.length()] == '/') { // This pathname falls within this mount system. Filename local_filename = strpath.substr(mount_point.length() + 1); Filename local_filename_pz = strpath_pz.substr(mount_point.length() + 1); if (consider_match(found_file, composite_file, mount, local_filename, pathname, false, open_flags)) { return found_file; } #ifdef HAVE_ZLIB if (vfs_implicit_pz) { // Bingo! if (consider_match(found_file, composite_file, mount, local_filename_pz, pathname, true, open_flags)) { return found_file; } } #endif // HAVE_ZLIB } // If we discover that a file has been implicitly mounted during one of // the above operations, start over from the beginning of the loop. if (start_seq != _mount_seq) { start_seq = _mount_seq; i = _mounts.size(); } } if (found_file == (VirtualFile *)NULL && vfs_implicit_mf) { // The file wasn't found, as-is. Does it appear to be an implicit .mf // file reference? ((VirtualFileSystem *)this)->consider_mount_mf(filename); if (start_seq != _mount_seq) { // Yes, it was, or some nested file was. Now that we've implicitly // mounted the .mf file, go back and look again. return do_get_file(filename, open_flags); } } #if defined(_WIN32) && !defined(NDEBUG) if (!found_file) { // The file could not be found. Perhaps this is because the user passed // in a Windows-style path where a Unix-style path was expected? if (filename.length() > 2 && isalpha(filename[0]) && filename[1] == ':' && (filename[2] == '\\' || filename[2] == '/')) { Filename corrected_fn = Filename::from_os_specific(filename); if (corrected_fn.exists()) { express_cat.warning() << "Filename uses Windows-style path: " << filename << "\n"; express_cat.warning() << " expected Unix-style path: " << corrected_fn << "\n"; } } } #endif return found_file; } /** * Evaluates one possible filename match found during a get_file() operation. * There may be multiple matches for a particular filename due to the * ambiguities introduced by allowing multiple mount points, so we may have to * keep searching even after the first match is found. * * Returns true if the search should terminate now, or false if it should keep * iterating. */ bool VirtualFileSystem:: consider_match(PT(VirtualFile) &found_file, VirtualFileComposite *&composite_file, VirtualFileMount *mount, const Filename &local_filename, const Filename &original_filename, bool implicit_pz_file, int open_flags) const { PT(VirtualFile) vfile = mount->make_virtual_file(local_filename, original_filename, false, open_flags); if (!vfile->has_file() && ((open_flags & OF_allow_nonexist) == 0)) { // Keep looking. return false; } if (found_file == (VirtualFile *)NULL) { // This was our first match. Save it. found_file = vfile; if (!found_file->is_directory() || ((open_flags & OF_make_directory) != 0)) { // If it's not a directory (or we wanted to make a directory), we're // done. return true; } // It is a directory, so save it for later. if (implicit_pz_file) { // Don't look for directories named file.pz. found_file = NULL; } } else { // This was our second match. The previous match(es) must have been // directories. if (!vfile->is_directory()) { // However, this one isn't a directory. We're done. return true; } if (!implicit_pz_file) { // At least two directories matched to the same path. We need a // composite directory. if (composite_file == (VirtualFileComposite *)NULL) { composite_file = new VirtualFileComposite((VirtualFileSystem *)this, found_file->get_original_filename()); composite_file->set_original_filename(original_filename); composite_file->add_component(found_file); found_file = composite_file; } composite_file->add_component(vfile); } } // Keep going, looking for more directories. return false; } /** * The indicated filename was not found. Check to see if it is using an * implicit reference to a .mf file as a directory, that hasn't already been * mounted. If it is, mount the .mf file in-place, and return true; if it is * not, or if its .mf file is already mounted in-place, return false. * * Assumes the lock is already held. */ bool VirtualFileSystem:: consider_mount_mf(const Filename &filename) { Filename dirname = filename.get_dirname(); if (dirname.empty() || dirname == filename) { // Reached the top directory; no .mf file references. return false; } if (is_directory(dirname)) { // Reached a real (or already-mounted) directory; no unmounted .mf file // references. return false; } if (dirname.get_extension() == "mf") { // Hey, here's a multifile reference! dirname.set_binary(); PT(VirtualFile) file = do_get_file(dirname, false); if (file == (VirtualFile *)NULL || !file->is_regular_file()) { // Oh, never mind. Not a real file. return false; } PT(Multifile) multifile = new Multifile; istream *stream = file->open_read_file(false); if (stream == (istream *)NULL) { // Couldn't read file. return false; } // Wrap a thread-safe wrapper around that stream, so multiple threads can // safely read the multifile simultaneously. IStreamWrapper *streamw = new IStreamWrapper(stream, true); if (!multifile->open_read(streamw, true)) { // Invalid multifile. return false; } multifile->set_multifile_name(dirname.get_basename()); express_cat->info() << "Implicitly mounting " << dirname << "\n"; PT(VirtualFileMountMultifile) new_mount = new VirtualFileMountMultifile(multifile); return do_mount(new_mount, dirname, MF_read_only); } // Recurse. return consider_mount_mf(dirname); }