1 /******************************************************************************/
2 /* Mednafen - Multi-system Emulator */
3 /******************************************************************************/
4 /* NativeVFS.cpp:
5 ** Copyright (C) 2018-2019 Mednafen Team
6 **
7 ** This program is free software; you can redistribute it and/or
8 ** modify it under the terms of the GNU General Public License
9 ** as published by the Free Software Foundation; either version 2
10 ** of the License, or (at your option) any later version.
11 **
12 ** This program is distributed in the hope that it will be useful,
13 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ** GNU General Public License for more details.
16 **
17 ** You should have received a copy of the GNU General Public License
18 ** along with this program; if not, write to the Free Software Foundation, Inc.,
19 ** 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 */
21
22 #include <mednafen/mednafen.h>
23 #include <mednafen/NativeVFS.h>
24 #include <mednafen/FileStream.h>
25
26 #ifdef WIN32
27 #include <mednafen/win32-common.h>
28 #else
29 #include <unistd.h>
30 #include <dirent.h>
31 #endif
32
33 #include <sys/types.h>
34 #include <sys/stat.h>
35
36 namespace Mednafen
37 {
38
NativeVFS()39 NativeVFS::NativeVFS() : VirtualFS(MDFN_PS, (PSS_STYLE == 2) ? "\\/" : PSS)
40 {
41
42
43 }
44
~NativeVFS()45 NativeVFS::~NativeVFS()
46 {
47
48
49 }
50
open(const std::string & path,const uint32 mode,const int do_lock,const bool throw_on_noent,const CanaryType canary)51 Stream* NativeVFS::open(const std::string& path, const uint32 mode, const int do_lock, const bool throw_on_noent, const CanaryType canary)
52 {
53 if(canary != CanaryType::open)
54 _exit(-1);
55
56 try
57 {
58 return new FileStream(path, mode, do_lock);
59 }
60 catch(MDFN_Error& e)
61 {
62 if(e.GetErrno() != ENOENT || throw_on_noent)
63 throw;
64
65 return nullptr;
66 }
67 }
68
mkdir(const std::string & path,const bool throw_on_exist)69 bool NativeVFS::mkdir(const std::string& path, const bool throw_on_exist)
70 {
71 if(path.find('\0') != std::string::npos)
72 throw MDFN_Error(EINVAL, _("Error creating directory \"%s\": %s"), path.c_str(), _("Null character in path."));
73
74 #ifdef WIN32
75 bool invalid_utf8;
76 std::u16string u16path = UTF8_to_UTF16(path, &invalid_utf8, true);
77
78 if(invalid_utf8)
79 throw MDFN_Error(EINVAL, _("Error creating directory \"%s\": %s"), path.c_str(), _("Invalid UTF-8"));
80
81 if(::_wmkdir((const wchar_t*)u16path.c_str()))
82 #elif defined HAVE_MKDIR
83 #if MKDIR_TAKES_ONE_ARG
84 if(::mkdir(path.c_str()))
85 #else
86 if(::mkdir(path.c_str(), S_IRWXU))
87 #endif
88 #else
89 #error "mkdir() missing?!"
90 #endif
91 {
92 ErrnoHolder ene(errno);
93
94 if(ene.Errno() != EEXIST || throw_on_exist)
95 throw MDFN_Error(ene.Errno(), _("Error creating directory \"%s\": %s"), path.c_str(), ene.StrError());
96
97 return false;
98 }
99
100 return true;
101 }
102
103
unlink(const std::string & path,const bool throw_on_noent,const CanaryType canary)104 bool NativeVFS::unlink(const std::string& path, const bool throw_on_noent, const CanaryType canary)
105 {
106 if(canary != CanaryType::unlink)
107 _exit(-1);
108
109 if(path.find('\0') != std::string::npos)
110 throw MDFN_Error(EINVAL, _("Error unlinking \"%s\": %s"), path.c_str(), _("Null character in path."));
111
112 #ifdef WIN32
113 bool invalid_utf8;
114 std::u16string u16path = UTF8_to_UTF16(path, &invalid_utf8, true);
115
116 if(invalid_utf8)
117 throw MDFN_Error(EINVAL, _("Error unlinking \"%s\": %s"), path.c_str(), _("Invalid UTF-8"));
118
119 if(::_wunlink((const wchar_t*)u16path.c_str()))
120 #else
121 if(::unlink(path.c_str()))
122 #endif
123 {
124 ErrnoHolder ene(errno);
125
126 if(ene.Errno() == ENOENT && !throw_on_noent)
127 return false;
128 else
129 throw MDFN_Error(ene.Errno(), _("Error unlinking \"%s\": %s"), path.c_str(), ene.StrError());
130 }
131
132 return true;
133 }
134
rename(const std::string & oldpath,const std::string & newpath,const CanaryType canary)135 void NativeVFS::rename(const std::string& oldpath, const std::string& newpath, const CanaryType canary)
136 {
137 if(canary != CanaryType::rename)
138 _exit(-1);
139
140 if(oldpath.find('\0') != std::string::npos || newpath.find('\0') != std::string::npos)
141 throw MDFN_Error(EINVAL, _("Error renaming \"%s\" to \"%s\": %s"), oldpath.c_str(), newpath.c_str(), _("Null character in path."));
142
143 #ifdef WIN32
144 bool invalid_utf8_old;
145 bool invalid_utf8_new;
146 std::u16string u16oldpath = UTF8_to_UTF16(oldpath, &invalid_utf8_old, true);
147 std::u16string u16newpath = UTF8_to_UTF16(newpath, &invalid_utf8_new, true);
148
149 if(invalid_utf8_old || invalid_utf8_new)
150 throw MDFN_Error(EINVAL, _("Error renaming \"%s\" to \"%s\": %s"), oldpath.c_str(), newpath.c_str(), _("Invalid UTF-8"));
151
152 if(::_wrename((const wchar_t*)u16oldpath.c_str(), (const wchar_t*)u16newpath.c_str()))
153 {
154 ErrnoHolder ene(errno);
155
156 // TODO/FIXME: Ensure oldpath and newpath don't refer to the same file via symbolic link.
157 if(oldpath != newpath && (ene.Errno() == EACCES || ene.Errno() == EEXIST))
158 {
159 if(::_wunlink((const wchar_t*)u16newpath.c_str()) || _wrename((const wchar_t*)u16oldpath.c_str(), (const wchar_t*)u16newpath.c_str()))
160 throw MDFN_Error(ene.Errno(), _("Error renaming \"%s\" to \"%s\": %s"), oldpath.c_str(), newpath.c_str(), ene.StrError());
161 }
162 else
163 throw MDFN_Error(ene.Errno(), _("Error renaming \"%s\" to \"%s\": %s"), oldpath.c_str(), newpath.c_str(), ene.StrError());
164 }
165 #else
166
167 if(::rename(oldpath.c_str(), newpath.c_str()))
168 {
169 ErrnoHolder ene(errno);
170
171 throw MDFN_Error(ene.Errno(), _("Error renaming \"%s\" to \"%s\": %s"), oldpath.c_str(), newpath.c_str(), ene.StrError());
172 }
173
174 #endif
175 }
176
finfo(const std::string & path,FileInfo * fi,const bool throw_on_noent)177 bool NativeVFS::finfo(const std::string& path, FileInfo* fi, const bool throw_on_noent)
178 {
179 if(path.find('\0') != std::string::npos)
180 throw MDFN_Error(EINVAL, _("Error getting file information for \"%s\": %s"), path.c_str(), _("Null character in path."));
181 //
182 //
183 #ifdef WIN32
184 bool invalid_utf8;
185 std::u16string u16path = UTF8_to_UTF16(path, &invalid_utf8, true);
186 struct _stati64 buf;
187
188 if(invalid_utf8)
189 throw MDFN_Error(EINVAL, _("Error getting file information for \"%s\": %s"), path.c_str(), _("Invalid UTF-8"));
190
191 if(::_wstati64((const wchar_t*)u16path.c_str(), &buf))
192 #else
193 struct stat buf;
194 if(::stat(path.c_str(), &buf))
195 #endif
196 {
197 ErrnoHolder ene(errno);
198
199 if(ene.Errno() == ENOENT && !throw_on_noent)
200 return false;
201
202 throw MDFN_Error(ene.Errno(), _("Error getting file information for \"%s\": %s"), path.c_str(), ene.StrError());
203 }
204
205 if(fi)
206 {
207 FileInfo new_fi;
208
209 new_fi.size = buf.st_size;
210 new_fi.mtime_us = (int64)buf.st_mtime * 1000 * 1000;
211
212 new_fi.is_regular = S_ISREG(buf.st_mode);
213 new_fi.is_directory = S_ISDIR(buf.st_mode);
214
215 *fi = new_fi;
216 }
217
218 return true;
219 }
220
readdirentries(const std::string & path,std::function<bool (const std::string &)> callb)221 void NativeVFS::readdirentries(const std::string& path, std::function<bool(const std::string&)> callb)
222 {
223 if(path.find('\0') != std::string::npos)
224 throw MDFN_Error(EINVAL, _("Error reading directory entries from \"%s\": %s"), path.c_str(), _("Null character in path."));
225 //
226 //
227 #ifdef WIN32
228 //
229 // TODO: drive-relative? probably would need to change how we represent and compose paths in Mednafen...
230 //
231 HANDLE dp = nullptr;
232 bool invalid_utf8;
233 std::u16string u16path = UTF8_to_UTF16(path + preferred_path_separator + '*', &invalid_utf8, true);
234 WIN32_FIND_DATAW ded;
235
236 if(invalid_utf8)
237 throw MDFN_Error(EINVAL, _("Error reading directory entries from \"%s\": %s"), path.c_str(), _("Invalid UTF-8"));
238
239 try
240 {
241 if(!(dp = FindFirstFileW((const wchar_t*)u16path.c_str(), &ded)))
242 {
243 const uint32 ec = GetLastError();
244
245 throw MDFN_Error(0, _("Error reading directory entries from \"%s\": %s"), path.c_str(), Win32Common::ErrCodeToString(ec).c_str());
246 }
247
248 for(;;)
249 {
250 //printf("%s\n", UTF16_to_UTF8((const char16_t*)ded.cFileName, nullptr, true).c_str());
251 if(!callb(UTF16_to_UTF8((const char16_t*)ded.cFileName, nullptr, true)))
252 break;
253 //
254 if(!FindNextFileW(dp, &ded))
255 {
256 const uint32 ec = GetLastError();
257
258 if(ec == ERROR_NO_MORE_FILES)
259 break;
260 else
261 throw MDFN_Error(0, _("Error reading directory entries from \"%s\": %s"), path.c_str(), Win32Common::ErrCodeToString(ec).c_str());
262 }
263 }
264 //
265 FindClose(dp);
266 dp = nullptr;
267 }
268 catch(...)
269 {
270 if(dp)
271 {
272 FindClose(dp);
273 dp = nullptr;
274 }
275 throw;
276 }
277 #else
278 DIR* dp = nullptr;
279 std::string fname;
280
281 fname.reserve(512);
282
283 try
284 {
285 if(!(dp = opendir(path.c_str())))
286 {
287 ErrnoHolder ene(errno);
288
289 throw MDFN_Error(ene.Errno(), _("Error reading directory entries from \"%s\": %s"), path.c_str(), ene.StrError());
290 }
291 //
292 for(;;)
293 {
294 struct dirent* de;
295
296 errno = 0;
297 if(!(de = readdir(dp)))
298 {
299 if(errno)
300 {
301 ErrnoHolder ene(errno);
302
303 throw MDFN_Error(ene.Errno(), _("Error reading directory entries from \"%s\": %s"), path.c_str(), ene.StrError());
304 }
305 break;
306 }
307 //
308 fname.clear();
309 fname += de->d_name;
310 //
311 if(!callb(fname))
312 break;
313 }
314 //
315 closedir(dp);
316 dp = nullptr;
317 }
318 catch(...)
319 {
320 if(dp)
321 {
322 closedir(dp);
323 dp = nullptr;
324 }
325 throw;
326 }
327 #endif
328 }
329
330 // Really dumb, maybe we should use boost?
is_absolute_path(const std::string & path)331 bool NativeVFS::is_absolute_path(const std::string& path)
332 {
333 if(!path.size())
334 return false;
335
336 if(is_path_separator(path[0]))
337 return true;
338
339 #if defined(WIN32) || defined(DOS)
340 if(path.size() >= 2 && path[1] == ':')
341 {
342 if((path[0] >= 'a' && path[0] <= 'z') || (path[0] >= 'A' && path[0] <= 'Z'))
343 return true;
344 }
345 #endif
346
347 return false;
348 }
349
check_firop_safe(const std::string & path)350 void NativeVFS::check_firop_safe(const std::string& path)
351 {
352 //
353 // First, check for any 8-bit characters, and print a warning about portability.
354 //
355 for(size_t x = 0; x < path.size(); x++)
356 {
357 if(path[x] & 0x80)
358 {
359 MDFN_printf(_("WARNING: Referenced path \"%s\" contains at least one 8-bit non-ASCII character; this may cause portability issues.\n"), path.c_str());
360 break;
361 }
362 }
363
364 if(!MDFN_GetSettingB("filesys.untrusted_fip_check"))
365 return;
366
367 // We could make this more OS-specific, but it shouldn't hurt to try to weed out usage of characters that are path
368 // separators in one OS but not in another, and we'd also run more of a risk of missing a special path separator case
369 // in some OS.
370 std::string unsafe_reason;
371
372 #ifdef WIN32
373 if(!UTF8_validate(path, true))
374 unsafe_reason += _("Invalid UTF-8. ");
375 #endif
376
377 if(path.find('\0') != std::string::npos)
378 unsafe_reason += _("Contains null(0). ");
379
380 if(path.find(':') != std::string::npos)
381 unsafe_reason += _("Contains colon. ");
382
383 if(path.find('\\') != std::string::npos)
384 unsafe_reason += _("Contains backslash. ");
385
386 if(path.find('/') != std::string::npos)
387 unsafe_reason += _("Contains forward slash. ");
388
389 if(path == "..")
390 unsafe_reason += _("Is parent directory. ");
391
392 #if defined(DOS) || defined(WIN32)
393 //
394 // http://support.microsoft.com/kb/74496
395 // http://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
396 //
397 {
398 static const char* dev_names[] =
399 {
400 "CON", "PRN", "AUX", "CLOCK$", "NUL", "CONIN$", "CONOUT$",
401 "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³",
402 "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³",
403 NULL
404 };
405 //
406 const char* pcs = path.c_str();
407 for(const char** ls = dev_names; *ls != NULL; ls++)
408 {
409 size_t lssl = strlen(*ls);
410
411 if(!MDFN_strazicmp(*ls, pcs, lssl))
412 {
413 if(pcs[lssl] == 0 || pcs[lssl] == ':' || pcs[lssl] == '.' || pcs[lssl] == ' ')
414 {
415 unsafe_reason += _("Is (likely) a reserved device name. ");
416 break;
417 }
418 }
419 }
420 }
421 #endif
422
423 if(unsafe_reason.size() > 0)
424 throw MDFN_Error(0, _("Referenced path \"%s\" (escaped) is potentially unsafe. %s Refer to the documentation about the \"filesys.untrusted_fip_check\" setting.\n"), MDFN_strescape(path).c_str(), unsafe_reason.c_str());
425 }
426
get_file_path_components(const std::string & file_path,std::string * dir_path_out,std::string * file_base_out,std::string * file_ext_out)427 void NativeVFS::get_file_path_components(const std::string &file_path, std::string* dir_path_out, std::string* file_base_out, std::string *file_ext_out)
428 {
429 #if defined(WIN32) || defined(DOS)
430 if(file_path.size() >= 3 && ((file_path[0] >= 'a' && file_path[0] <= 'z') || (file_path[0] >= 'A' && file_path[0] <= 'Z')) && file_path[1] == ':' && file_path.find_last_of(allowed_path_separators) == std::string::npos)
431 {
432 VirtualFS::get_file_path_components(file_path.substr(0, 2) + '.' + preferred_path_separator + file_path.substr(2), dir_path_out, file_base_out, file_ext_out);
433 return;
434 }
435 #endif
436
437 VirtualFS::get_file_path_components(file_path, dir_path_out, file_base_out, file_ext_out);
438 }
439
440 }
441