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
|