// Filename: virtualFileSystem.cxx // Created by: drose (03Aug02) // //////////////////////////////////////////////////////////////////// // // PANDA 3D SOFTWARE // Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved // // All use of this software is subject to the terms of the Panda 3d // Software license. You should have received a copy of this license // along with this source code; you will also find a current copy of // the license at http://etc.cmu.edu/panda3d/docs/license/ . // // To contact the maintainers of this program write to // panda3d-general@lists.sourceforge.net . // //////////////////////////////////////////////////////////////////// #include "virtualFileSystem.h" #include "virtualFileMount.h" #include "virtualFileMountMultifile.h" #include "virtualFileMountSystem.h" #include "dSearchPath.h" #include "dcast.h" #include "config_express.h" #include "executionEnvironment.h" #include "pset.h" VirtualFileSystem *VirtualFileSystem::_global_ptr = NULL; //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::Constructor // Access: Published // Description: //////////////////////////////////////////////////////////////////// VirtualFileSystem:: VirtualFileSystem() { _cwd = "/"; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::Destructor // Access: Published // Description: //////////////////////////////////////////////////////////////////// VirtualFileSystem:: ~VirtualFileSystem() { unmount_all(); } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::mount // Access: Published // Description: Mounts the indicated Multifile at the given mount // point. //////////////////////////////////////////////////////////////////// bool VirtualFileSystem:: mount(Multifile *multifile, const string &mount_point, int flags) { VirtualFileMountMultifile *mount = new VirtualFileMountMultifile(this, multifile, normalize_mount_point(mount_point), flags); _mounts.push_back(mount); return true; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::mount // Access: Published // Description: 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. // // 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 string &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()) { VirtualFileMountSystem *mount = new VirtualFileMountSystem(this, physical_filename, normalize_mount_point(mount_point), flags); _mounts.push_back(mount); return true; } 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)) { delete multifile; return false; } return mount(multifile, mount_point, flags); } } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::unmount // Access: Published // Description: Unmounts all appearances of the indicated Multifile // from the file system. Returns the number of // appearances unmounted. //////////////////////////////////////////////////////////////////// int VirtualFileSystem:: unmount(Multifile *multifile) { 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. delete mount; } 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()); return num_removed; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::unmount // Access: Published // Description: Unmounts all appearances of the indicated physical // filename (either a directory name or a Multifile // name) from the file system. Returns the number of // appearances unmounted. //////////////////////////////////////////////////////////////////// int VirtualFileSystem:: unmount(const Filename &physical_filename) { Mounts::iterator ri, wi; wi = ri = _mounts.begin(); while (ri != _mounts.end()) { VirtualFileMount *mount = (*ri); (*wi) = mount; if (mount->get_physical_filename() == physical_filename) { // Remove this one. Don't increment wi. delete mount; } else { // Don't remove this one. ++wi; } ++ri; } int num_removed = _mounts.end() - wi; _mounts.erase(wi, _mounts.end()); return num_removed; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::unmount_point // Access: Published // Description: Unmounts all systems attached to the given mount // point from the file system. Returns the number of // appearances unmounted. //////////////////////////////////////////////////////////////////// int VirtualFileSystem:: unmount_point(const string &mount_point) { 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. delete mount; } else { // Don't remove this one. ++wi; } ++ri; } int num_removed = _mounts.end() - wi; _mounts.erase(wi, _mounts.end()); return num_removed; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::unmount_all // Access: Published // Description: Unmounts all files from the file system. Returns the // number of systems unmounted. //////////////////////////////////////////////////////////////////// int VirtualFileSystem:: unmount_all() { Mounts::iterator mi; for (mi = _mounts.begin(); mi != _mounts.end(); ++mi) { VirtualFileMount *mount = (*mi); delete mount; } int num_removed = _mounts.size(); _mounts.clear(); return num_removed; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::chdir // Access: Published // Description: Changes the current directory. This is used to // resolve relative pathnames in get_file() and/or // find_file(). Returns true if successful, false // otherwise. // // This accepts a string rather than a Filename simply // for programmer convenience from the Python prompt. //////////////////////////////////////////////////////////////////// bool VirtualFileSystem:: chdir(const string &new_directory) { if (new_directory == "/") { // We can always return to the root. _cwd = new_directory; return true; } PT(VirtualFile) file = get_file(new_directory); if (file != (VirtualFile *)NULL && file->is_directory()) { _cwd = file->get_filename(); return true; } return false; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::get_cwd // Access: Published // Description: Returns the current directory name. See chdir(). //////////////////////////////////////////////////////////////////// const Filename &VirtualFileSystem:: get_cwd() const { return _cwd; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::get_file // Access: Published // Description: 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. //////////////////////////////////////////////////////////////////// PT(VirtualFile) VirtualFileSystem:: get_file(const Filename &filename) const { nassertr(!filename.empty(), NULL); Filename pathname(filename); if (pathname.is_local()) { pathname = Filename(_cwd, filename); } pathname.standardize(); string strpath = pathname.get_fullpath().substr(1); // 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; Mounts::const_reverse_iterator rmi; for (rmi = _mounts.rbegin(); rmi != _mounts.rend(); ++rmi) { VirtualFileMount *mount = (*rmi); string 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 (found_match(found_file, composite_file, mount, "")) { return found_file; } } else if (mount_point.empty()) { // This is the root mount point; all files are in here. if (mount->has_file(strpath)) { // Bingo! if (found_match(found_file, composite_file, mount, strpath)) { return found_file; } } } else if (strpath.length() > mount_point.length() && strpath.substr(0, mount_point.length()) == mount_point && strpath[mount_point.length()] == '/') { // This pathname falls within this mount system. Filename local_filename = strpath.substr(mount_point.length() + 1); if (mount->has_file(local_filename)) { // Bingo! if (found_match(found_file, composite_file, mount, local_filename)) { return found_file; } } } } return found_file; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::find_file // Access: Published // Description: 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) const { if (!filename.is_local()) { return get_file(filename); } 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); if (found_file != (VirtualFile *)NULL) { return found_file; } } return NULL; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::resolve_filename // Access: Public // Description: 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.get_fullpath(), searchpath); 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.get_fullpath(), searchpath); } } } 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); } } } if (!found.is_null()) { filename = found->get_filename(); return true; } return false; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::find_all_files // Access: Public // Description: 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; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::write // Access: Published // Description: //////////////////////////////////////////////////////////////////// void VirtualFileSystem:: write(ostream &out) const { Mounts::const_iterator mi; for (mi = _mounts.begin(); mi != _mounts.end(); ++mi) { VirtualFileMount *mount = (*mi); mount->write(out); } } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::get_global_ptr // Access: Published, Static // Description: 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) { _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", "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 = 0; string password; // 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); _global_ptr->mount(physical_filename, mount_point, flags, password); } } } return _global_ptr; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::close_read_file // Access: Published // Description: 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) const { 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. #ifndef NDEBUG stream->~istream(); (*global_operator_delete)(stream); #else delete stream; #endif } } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::scan_mount_points // Access: Public // Description: 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); } } } } } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::normalize_mount_point // Access: Private // Description: 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. //////////////////////////////////////////////////////////////////// Filename VirtualFileSystem:: normalize_mount_point(const string &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); } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::found_match // Access: Private // Description: Evaluates one 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:: found_match(PT(VirtualFile) &found_file, VirtualFileComposite *&composite_file, VirtualFileMount *mount, const string &local_filename) const { if (found_file == (VirtualFile *)NULL) { // This was our first match. Save it. found_file = new VirtualFileSimple(mount, local_filename); if (!mount->is_directory(local_filename)) { // If it's not a directory, we're done. return true; } } else { // This was our second match. The previous match(es) must // have been directories. if (!mount->is_directory(local_filename)) { // However, this one isn't a directory. We're done. return true; } // 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_filename()); composite_file->add_component(found_file); found_file = composite_file; } composite_file->add_component(new VirtualFileSimple(mount, local_filename)); } // Keep going, looking for more directories. return false; } //////////////////////////////////////////////////////////////////// // Function: VirtualFileSystem::parse_option // Access: Private, Static // Description: 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"; } }