#pragma region CPL License /* Nuclex Native Framework Copyright (C) 2002-2013 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library 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 IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License #ifndef NUCLEX_STORAGE_FILESYSTEM_PATHHELPER_H #define NUCLEX_STORAGE_FILESYSTEM_PATHHELPER_H #include "Nuclex/Storage/Config.h" #include #include namespace Nuclex { namespace Storage { namespace FileSystem { // ------------------------------------------------------------------------------------------- // /// Contains helper methods for working with paths /// /// A class describing the platform dependent path format that will be used /// /// /// /// We make some assumptions in this generalization. One assumption is that /// path separators will always consist of 1 ascii character. No UTF-8 characters /// and no multi-character path separators. /// /// /// There are some assumptions we must not make however. Here's a list of things /// that should be taken into account when extending this class: /// /// /// /// Not all platforms have a current working directory. WinRT (Microsoft's new /// Win32 API replacement) does not have one, for example. /// /// /// /// /// Platforms may have multiple valid path separators. Windows prefers the /// backward slash, but most of the time also understands the forward slash. /// /// /// /// /// Not all platforms have a drive letter. Linux simply has a topmost directory /// that maps to some file system and any other file systems (stored on another /// hard drive or a ram disk, for example) can be mapped into directories. /// /// /// /// /// template class PathHelper { /// Determines if the specified character is a path separator /// Character that will be checked for being a path separator /// True if the specified character is a path separator public: static bool IsPathSeparator(char character) { return TPathFormat::IsPathSeparator(character); } /// Determines whether the specified path ends with a slash /// Path that will be checked for ending with a slash /// True if the path ends with a slash public: static bool HasTrailingPathSeparator(const std::string &path) { std::size_t length = path.length(); if(length > 0) { return TPathFormat::IsPathSeparator(path[length - 1]); } else { return false; } } /// Removes the trailing slash from a path /// Path from which the trailing slash will be removed public: static void RemoveTrailingPathSeparator(std::string &path) { std::size_t length = path.length(); if(length > 0) { if(TPathFormat::IsPathSeparator(path[length - 1])) { path.resize(length - 1); } } } /// Adds the trailing slash to a path /// Path to which the trailing slash will be added public: static void AddTrailingPathSeparator(std::string &path) { std::size_t length = path.length(); if(length > 0) { if(!TPathFormat::IsPathSeparator(path[length - 1])) { static const char pathSeparatorChars[] = { TPathFormat::PathSeparator, '\0' }; path.append(pathSeparatorChars, 1); } } } /// Combines a directory with a file name /// Directory that will be combined /// File name that will be appended to the directory /// The full path of the file public: static std::string CombinePaths( const std::string &directory, const std::string &filename ) { if(TPathFormat::IsAbsolutePath(filename)) { return filename; } else if(HasTrailingPathSeparator(directory)) { return directory + filename; } else { return directory + TPathFormat::PathSeparator + filename; } } /// Determines if the specified path is an absolute path /// Path that will be checked /// True if the specified path was an absolute path public: static bool IsAbsolutePath(const std::string &path) { return TPathFormat::IsAbsolutePath(path); } /// Extracts the last element from a path /// Path from which the last element will be extracted /// The last element of the specified path public: static std::string GetLastPathElement(const std::string &path) { std::size_t length = path.length(); // If the path ends with a path separator, cut it off so that the last element // from a path like "/tmp/xyz/" becomes "xyz". if(TPathFormat::IsPathSeparator(path[length - 1])) { --length; } // Reverse scan the path for the last separator for(std::size_t index = length - 1; index > 0; --index) { if(TPathFormat::IsPathSeparator(path[index])) { ++index; // Skip the path separator we just found return path.substr(index, length - index); } } return path.substr(0, length); } /// Removes the last element from a path /// Path from which the last element will be removed /// The specified path minus its last element public: static std::string RemoveLastElement(const std::string &path) { std::size_t length = path.length(); // If the path ends with a path separator, cut it off so that the last element // from a path like "/tmp/xyz/" becomes "xyz". if(TPathFormat::IsPathSeparator(path[length - 1])) { --length; } // Reverse scan the path for the last separator for(std::size_t index = length - 1; index > 0; --index) { if(TPathFormat::IsPathSeparator(path[index])) { return path.substr(0, index); } } return std::string(); } /// Turns a relative path into an absolute one /// /// Path starting from which the relative path will be resolved /// /// Relative path that will be resolved /// The absolute path the relative path pointed to public: static std::string MakeAbsolute( const std::string &basePath, const std::string &relativePath ) { std::size_t relativePathLength = relativePath.length(); // If the relative path is empty, the base path becomes the absolute path if(relativePathLength < 1) { return basePath; } // If the relative path starts with a path separator, the user wants // to go to the root directory. if(TPathFormat::IsPathSeparator(relativePath[0])) { std::size_t prefixLength = TPathFormat::GetAbsolutePathPrefixLength(basePath); return unwindRelativePath(basePath.substr(0, prefixLength), relativePath.substr(1)); } // The relative path might actually contain an absolute path. In that case, // we will discard the base path and return the relative path std::size_t prefixLength = TPathFormat::GetAbsolutePathPrefixLength(relativePath); if(prefixLength > 0) { // If relativePath contains a drive letter, it may be that the path begins // at the current working directory (if it is on that drive) or from the root. // On non-Windows systems the check will always returns false. if(TPathFormat::ContainsDriveLetter(relativePath)) { return unwindPathWithDriveLetter(basePath, relativePath); } // Call unwind in any case so that a relative path like "/tmp/../home/" will be // unwound into "/home/". return unwindRelativePath( relativePath.substr(0, prefixLength), relativePath.substr(prefixLength) ); } // if relative path is absolute // The relative path was really relative and didn't contain a drive letter either, // so resolve it normally starting from the base path return unwindRelativePath(basePath, relativePath); } /// Unwinds a relative path pointing to another drive letter /// /// Path starting from which the relative path will be resolved /// /// Relative path that will be resolved /// The absolute path the relative path pointed to /// /// This method will only be used on systems that have drive letters (only Windows /// at the time I write this). /// private: static std::string unwindPathWithDriveLetter( const std::string &basePath, const std::string &relativePath ) { using namespace std; std::size_t relativePathLength = relativePath.length(); assert((relativePathLength > 1) && "Relative path has to be at least 2 characters long"); // If it starts with a drive letter and a path separator it's an absolute path already if(relativePathLength > 2) { if(TPathFormat::IsPathSeparator(relativePath[2])) { return relativePath; } } // Otherwise, if it's on a different drive than the base directory and we need to // either start from the root or the current working directory if(basePath[0] != relativePath[0]) { // If the process' current working basePath is on the drive specified // by the relative path, unwind it relative to the working basePath if(TPathFormat::HasCurrentWorkingDirectory) { std::string workingDirectory = TPathFormat::GetCurrentWorkingDirectory(); if(workingDirectory.length() > 1) { if(workingDirectory[0] == relativePath[0]) { return unwindRelativePath(workingDirectory, relativePath.substr(2)); } } } // The relative path specifies a drive that neither is the reference location // nor the current working directory. Start from the root directory on that drive. return unwindRelativePath( relativePath.substr(0, 2) + TPathFormat::PathSeparator, relativePath.substr(2) ); } // The relativ path specifies the same drive as the reference location, so start // from the relative path return unwindRelativePath(basePath, relativePath.substr(2)); } /// Unwinds the directory chain of a relative path /// Absolute starting directory to begin unwinding in /// Relative path that will be unwound /// The absolute path the unwinded relative path ended up in /// /// If you call this with "c:\test" and "..\data\mooh\..\test" or will return /// "c:\data\test" - basically it removes the ".." and "." in any path. It supports /// any number of dots, so "/tmp/my/invalid/dir/...." would turn into "/tmp". /// private: static std::string unwindRelativePath( const std::string &start, const std::string &relative ) { if(relative.length() == 0) { return start; } // Make sure the start path is valid. This method should be called with start being // an absolute path, but that's not a requirement. std::string unwound(start); if(unwound.length() == 0) { unwound = TPathFormat::PathSeparator; } else { char lastChar = unwound[unwound.length() - 1]; if(!TPathFormat::IsPathSeparator(lastChar)) { unwound += TPathFormat::PathSeparator; } } std::size_t rootPrefixLength = TPathFormat::GetAbsolutePathPrefixLength(unwound); // Process the relative path element by element, each time appending the element // or going upwards in the path built so far if dots are specified. std::size_t startIndex = 0; bool terminate = false; while(!terminate) { std::size_t endIndex; for(endIndex = startIndex; endIndex < relative.length(); ++endIndex) { if(TPathFormat::IsPathSeparator(relative[endIndex])) { break; } } if(endIndex == relative.length()) { terminate = true; } // If this element consists of dots only, it means we need to go upwards // in the directory structure. Otherwise, it's a directory or file name. std::size_t dotCount = 0; for(std::size_t index = startIndex; index < endIndex; ++index) { if(relative[index] == '.') { ++dotCount; } else { dotCount = std::string::npos; break; } } // Is this a normal path element that needs to be appended? if(dotCount == std::string::npos) { unwound += relative.substr(startIndex, endIndex - startIndex); if(!terminate) { unwound += TPathFormat::PathSeparator; } } else { // Nope, we need to go upwards // Locate the point at which we need to cut off the path we built so far if(unwound.length() > 0) { std::size_t cutOffIndex = unwound.length() - 1; for(; cutOffIndex > rootPrefixLength; --cutOffIndex) { if(TPathFormat::IsPathSeparator(unwound[cutOffIndex])) { --cutOffIndex; break; } } while(dotCount > 1) { --dotCount; // If we're already at the root directory level, discard all dots // and stay at this level -- there is no further going up! if(cutOffIndex == rootPrefixLength) { break; } for(; cutOffIndex > rootPrefixLength; --cutOffIndex) { if(TPathFormat::IsPathSeparator(unwound[cutOffIndex])) { --cutOffIndex; break; } } } // Unless there was nothing to cut off (zero or one dots), cut the unwound path off, // walking upwards in the directory hierarchy. if(cutOffIndex != std::string::npos) { unwound = unwound.substr(0, cutOffIndex + 1); } } } startIndex = endIndex + 1; } return unwound; } }; // ------------------------------------------------------------------------------------------- // }}} // namespace Nuclex::Storage::FileSystem #endif // NUCLEX_STORAGE_FILESYSTEM_PATHHELPER_H