LCOV - code coverage report
Current view: top level - source/lib - path.h (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 93 107 86.9 %
Date: 2023-01-19 00:18:29 Functions: 24 25 96.0 %

          Line data    Source code
       1             : /* Copyright (C) 2022 Wildfire Games.
       2             :  *
       3             :  * Permission is hereby granted, free of charge, to any person obtaining
       4             :  * a copy of this software and associated documentation files (the
       5             :  * "Software"), to deal in the Software without restriction, including
       6             :  * without limitation the rights to use, copy, modify, merge, publish,
       7             :  * distribute, sublicense, and/or sell copies of the Software, and to
       8             :  * permit persons to whom the Software is furnished to do so, subject to
       9             :  * the following conditions:
      10             :  *
      11             :  * The above copyright notice and this permission notice shall be included
      12             :  * in all copies or substantial portions of the Software.
      13             :  *
      14             :  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
      15             :  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
      16             :  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
      17             :  * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
      18             :  * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
      19             :  * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
      20             :  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      21             :  */
      22             : 
      23             : /*
      24             :  * Path string class, similar to boost::filesystem::basic_path.
      25             :  */
      26             : 
      27             : // notes:
      28             : // - this module is independent of lib/file so that it can be used from
      29             : //   other code without pulling in the entire file manager.
      30             : // - there is no restriction on buffer lengths except the underlying OS.
      31             : //   input buffers must not exceed PATH_MAX chars, while outputs
      32             : //   must hold at least that much.
      33             : // - unless otherwise mentioned, all functions are intended to work with
      34             : //   native and VFS paths.
      35             : //   when reading, both '/' and SYS_DIR_SEP are accepted; '/' is written.
      36             : 
      37             : #ifndef INCLUDED_PATH
      38             : #define INCLUDED_PATH
      39             : 
      40             : #include "lib/sysdep/os.h"
      41             : #include "lib/utf8.h"
      42             : 
      43             : #include <algorithm>
      44             : #include <cstring>
      45             : #if OS_WIN
      46             : #include <filesystem>
      47             : #endif
      48             : #include <functional>
      49             : 
      50             : namespace ERR
      51             : {
      52             :     const Status PATH_CHARACTER_ILLEGAL   = -100300;
      53             :     const Status PATH_CHARACTER_UNSAFE    = -100301;
      54             :     const Status PATH_NOT_FOUND           = -100302;
      55             :     const Status PATH_MIXED_SEPARATORS    = -100303;
      56             : }
      57             : 
      58             : /**
      59             :  * is s2 a subpath of s1, or vice versa? (equal counts as subpath)
      60             :  *
      61             :  * @param s1, s2 comparand strings
      62             :  * @return bool
      63             :  **/
      64             : bool path_is_subpath(const wchar_t* s1, const wchar_t* s2);
      65             : 
      66             : /**
      67             :  * Get the path component of a path.
      68             :  * Skips over all characters up to the last dir separator, if any.
      69             :  *
      70             :  * @param path Input path.
      71             :  * @return pointer to path component within \<path\>.
      72             :  **/
      73             : const wchar_t* path_name_only(const wchar_t* path);
      74             : 
      75             : 
      76             : // NB: there is a need for 'generic' paths (e.g. for Trace entry / archive pathnames).
      77             : // converting between specialized variants via c_str would be inefficient, and the
      78             : // Os/VfsPath types are hopefully sufficient to avoid errors.
      79      160850 : class Path
      80             : {
      81             : public:
      82             :     using String = std::wstring;
      83             : 
      84        4886 :     Path()
      85        4886 :     {
      86        4886 :         DetectSeparator();
      87        4886 :     }
      88             : 
      89       64690 :     Path(const Path& p)
      90       64690 :         : path(p.path)
      91             :     {
      92       64690 :         DetectSeparator();
      93       64690 :     }
      94             : 
      95        7443 :     Path(const char* p)
      96        7443 :         : path((const unsigned char*)p, (const unsigned char*)p+strlen(p))
      97             :         // interpret bytes as unsigned; makes no difference for ASCII,
      98             :         // and ensures OsPath on Unix will only contain values 0 <= c < 0x100
      99             :     {
     100        7443 :         DetectSeparator();
     101        7443 :     }
     102             : 
     103       17777 :     Path(const wchar_t* p)
     104       17777 :         : path(p, p+wcslen(p))
     105             :     {
     106       17777 :         DetectSeparator();
     107       17777 :     }
     108             : 
     109          22 :     Path(const std::string& s)
     110          22 :         : path((const unsigned char*)s.c_str(), (const unsigned char*)s.c_str()+s.length())
     111             :     {
     112          22 :         DetectSeparator();
     113          22 :     }
     114             : 
     115       66032 :     Path(const std::wstring& s)
     116       66032 :         : path(s)
     117             :     {
     118       66032 :         DetectSeparator();
     119       66032 :     }
     120             : 
     121        5499 :     Path& operator=(const Path& rhs)
     122             :     {
     123        5499 :         path = rhs.path;
     124        5499 :         DetectSeparator();  // (warns if separators differ)
     125        5499 :         return *this;
     126             :     }
     127             : 
     128       36556 :     bool empty() const
     129             :     {
     130       36556 :         return path.empty();
     131             :     }
     132             : 
     133             :     // TODO: This macro should be removed later when macOS supports std::filesystem.
     134             :     // Currently it does in more recent SDKs, but it also causes a slowdown on
     135             :     // OpenGL. See #6193.
     136             : #if OS_WIN
     137             :     /**
     138             :      * @returns a STL version of the path.
     139             :      */
     140             :     std::filesystem::path fileSystemPath() const
     141             :     {
     142             :         return std::filesystem::path(path);
     143             :     }
     144             : #endif
     145             : 
     146      103325 :     const String& string() const
     147             :     {
     148      103325 :         return path;
     149             :     }
     150             : 
     151             :     /**
     152             :      * Return a UTF-8 version of the path, in a human-readable but potentially
     153             :      * lossy form. It is *not* safe to take this string and construct a new
     154             :      * Path object from it (it may fail for some non-ASCII paths) - it should
     155             :      * only be used for displaying paths to users.
     156             :      */
     157        2827 :     std::string string8() const
     158             :     {
     159             :         Status err;
     160             : #if !OS_WIN
     161             :         // On Unix, assume paths consisting of 8-bit charactes saved in this wide string.
     162        5654 :         std::string spath(path.begin(), path.end());
     163             : 
     164             :         // Return it if it's valid UTF-8
     165        2827 :         wstring_from_utf8(spath, &err);
     166        2827 :         if(err == INFO::OK)
     167        2827 :             return spath;
     168             : 
     169             :         // Otherwise assume ISO-8859-1 and let utf8_from_wstring treat each character as a Unicode code point.
     170             : #endif
     171             :         // On Windows, paths are UTF-16 strings. We don't support non-BMP characters so we can assume it's simply a wstring.
     172           0 :         return utf8_from_wstring(path, &err);
     173             :     }
     174             : 
     175      105353 :     bool operator<(const Path& rhs) const
     176             :     {
     177      105353 :         return path < rhs.path;
     178             :     }
     179             : 
     180        9669 :     bool operator==(const Path& rhs) const
     181             :     {
     182        9669 :         return path == rhs.path;
     183             :     }
     184             : 
     185        3735 :     bool operator!=(const Path& rhs) const
     186             :     {
     187        3735 :         return !operator==(rhs);
     188             :     }
     189             : 
     190       27541 :     bool IsDirectory() const
     191             :     {
     192       27541 :         if(empty()) // (ensure length()-1 is safe)
     193          35 :             return true;    // (the VFS root directory is represented as an empty string)
     194       27506 :         return path[path.length()-1] == separator;
     195             :     }
     196             : 
     197        1721 :     Path Parent() const
     198             :     {
     199        1721 :         const size_t idxSlash = path.find_last_of(separator);
     200        1721 :         if(idxSlash == String::npos)
     201           0 :             return L"";
     202        1721 :         return path.substr(0, idxSlash);
     203             :     }
     204             : 
     205        6809 :     Path Filename() const
     206             :     {
     207        6809 :         const size_t idxSlash = path.find_last_of(separator);
     208        6809 :         if(idxSlash == String::npos)
     209        2338 :             return path;
     210        4471 :         return path.substr(idxSlash+1);
     211             :     }
     212             : 
     213         864 :     Path Basename() const
     214             :     {
     215        1728 :         const Path filename = Filename();
     216         864 :         const size_t idxDot = filename.string().find_last_of('.');
     217         864 :         if(idxDot == String::npos)
     218          18 :             return filename;
     219         846 :         return filename.string().substr(0, idxDot);
     220             :     }
     221             : 
     222             :     // (Path return type allows callers to use our operator==)
     223        5466 :     Path Extension() const
     224             :     {
     225       10932 :         const Path filename = Filename();
     226        5466 :         const size_t idxDot = filename.string().find_last_of('.');
     227        5466 :         if(idxDot == String::npos)
     228           0 :             return Path();
     229        5466 :         return filename.string().substr(idxDot);
     230             :     }
     231             : 
     232         855 :     Path ChangeExtension(Path extension) const
     233             :     {
     234         855 :         return Parent() / Path(Basename().string() + extension.string());
     235             :     }
     236             : 
     237       19383 :     Path operator/(Path rhs) const
     238             :     {
     239       19383 :         Path ret = *this;
     240       19383 :         if(ret.path.empty())    // (empty paths assume '/')
     241          35 :             ret.separator = rhs.separator;
     242       19383 :         if(!ret.IsDirectory())
     243        6050 :             ret.path += ret.separator;
     244             : 
     245       19383 :         if(rhs.path.find((ret.separator == '/')? '\\' : '/') != String::npos)
     246             :         {
     247           0 :             PrintToDebugOutput();
     248           0 :             rhs.PrintToDebugOutput();
     249           0 :             DEBUG_WARN_ERR(ERR::PATH_MIXED_SEPARATORS);
     250             :         }
     251       19383 :         ret.path += rhs.path;
     252       19383 :         return ret;
     253             :     }
     254             : 
     255             :     /**
     256             :      * Return the path before the common part of both paths
     257             :      * @param other Indicates the start of the path which should be removed
     258             :      * @note other should be a VfsPath, while this should be an OsPath
     259             :      */
     260         379 :     Path BeforeCommon(Path other) const
     261             :     {
     262         758 :         Path ret = *this;
     263         379 :         if(ret.empty() || other.empty())
     264           0 :             return L"";
     265             : 
     266             :         // Convert the separator to allow for string comparison
     267         379 :         if(other.separator != ret.separator)
     268           0 :             std::replace(other.path.begin(), other.path.end(), other.separator, ret.separator);
     269             : 
     270         379 :         const size_t idx = ret.path.rfind(other.path);
     271         379 :         if(idx == String::npos)
     272           0 :             return L"";
     273             : 
     274         379 :         return path.substr(0, idx);
     275             :     }
     276             : 
     277             :     static Status Validate(String::value_type c);
     278             : 
     279             : private:
     280           0 :     void PrintToDebugOutput() const
     281             :     {
     282           0 :         debug_printf("Path %s, separator %c\n", string8().c_str(), (char)separator);
     283           0 :     }
     284             : 
     285      166349 :     void DetectSeparator()
     286             :     {
     287      166349 :         const size_t idxBackslash = path.find('\\');
     288             : 
     289      166349 :         if(path.find('/') != String::npos && idxBackslash != String::npos)
     290             :         {
     291           0 :             PrintToDebugOutput();
     292           0 :             DEBUG_WARN_ERR(ERR::PATH_MIXED_SEPARATORS);
     293             :         }
     294             : 
     295             :         // (default to '/' for empty strings)
     296      166349 :         separator = (idxBackslash == String::npos)? '/' : '\\';
     297      166349 :     }
     298             : 
     299             :     String path;
     300             : 
     301             :     // note: ideally, path strings would only contain '/' or even SYS_DIR_SEP.
     302             :     // however, Windows-specific code (e.g. the sound driver detection)
     303             :     // uses these routines with '\\' strings. the boost::filesystem approach of
     304             :     // converting them all to '/' and then back via external_file_string is
     305             :     // annoying and inefficient. we allow either type of separators,
     306             :     // appending whichever was first encountered. when modifying the path,
     307             :     // we ensure the same separator is used.
     308             :     wchar_t separator = L'/';
     309             : };
     310             : 
     311             : static inline std::wostream& operator<<(std::wostream& s, const Path& path)
     312             : {
     313             :     s << path.string();
     314             :     return s;
     315             : }
     316             : 
     317             : static inline std::wistream& operator>>(std::wistream& s, Path& path)
     318             : {
     319             :     Path::String string;
     320             :     s >> string;
     321             :     path = Path(string);
     322             :     return s;
     323             : }
     324             : 
     325             : namespace std
     326             : {
     327             : template<>
     328             : struct hash<Path>
     329             : {
     330         268 :     std::size_t operator()(const Path& path) const
     331             :     {
     332         268 :         return m_StringHash(path.string());
     333             :     }
     334             : 
     335             : private:
     336             :     std::hash<std::wstring> m_StringHash;
     337             : };
     338             : }
     339             : 
     340             : #endif  // #ifndef INCLUDED_PATH

Generated by: LCOV version 1.13