openMSX
FileOperations.cc
Go to the documentation of this file.
1#ifdef _WIN32
2#ifndef _WIN32_IE
3#define _WIN32_IE 0x0500 // For SHGetSpecialFolderPathW with MinGW
4#endif
5#include "utf8_checked.hh"
6#include "vla.hh"
7#include <windows.h>
8#include <shlobj.h>
9#include <shellapi.h>
10#include <io.h>
11#include <direct.h>
12#include <ctype.h>
13#include <cstdlib>
14#include <cstring>
15#include <algorithm>
16#else // ifdef _WIN32_ ...
17#include <sys/types.h>
18#include <pwd.h>
19#include <climits>
20#include <unistd.h>
21#endif // ifdef _WIN32_ ... else ...
22
23#include "openmsx.hh" // for ad_printf
24
25#include "systemfuncs.hh"
26
27#if HAVE_NFTW
28#include <ftw.h>
29#endif
30
31#if defined(PATH_MAX)
32#define MAXPATHLEN PATH_MAX
33#elif defined(MAX_PATH)
34#define MAXPATHLEN MAX_PATH
35#else
36#define MAXPATHLEN 4096
37#endif
38
39
40#ifdef __APPLE__
41#include "FileOperationsMac.hh"
42#endif
43
44#include "ReadDir.hh"
45#include "FileOperations.hh"
46#include "FileException.hh"
47#include "StringOp.hh"
48#include "unistdp.hh"
49#include "one_of.hh"
50#include "ranges.hh"
51#include "strCat.hh"
52#include "build-info.hh"
53#include <algorithm>
54#include <array>
55#include <sstream>
56#include <cerrno>
57#include <cstdlib>
58#include <stdexcept>
59#include <cassert>
60#include <iterator>
61
62#ifndef _MSC_VER
63#include <dirent.h>
64#endif
65
66#if PLATFORM_ANDROID
67#include "SDL_system.h" // for GetExternalStorage stuff
68#endif
69
70using std::string;
71using std::string_view;
72
73#ifdef _WIN32
74using namespace utf8;
75#endif
76
78
79bool needsTildeExpansion(std::string_view path)
80{
81 return !path.empty() && (path[0] == '~');
82}
83
84string expandTilde(string path)
85{
86 if (!needsTildeExpansion(path)) {
87 return path;
88 }
89 auto pos = path.find_first_of('/');
90 string_view user = ((path.size() == 1) || (pos == 1))
91 ? string_view{}
92 : string_view(path).substr(1, (pos == string::npos) ? pos : pos - 1);
93 string result = getUserHomeDir(user);
94 if (result.empty()) {
95 // failed to find homedir, return the path unchanged
96 return path;
97 }
98 if (pos == string_view::npos) {
99 return result;
100 }
101 if (result.back() != '/') {
102 result += '/';
103 }
104 string_view last = string_view(path).substr(pos + 1);
105 result.append(last.data(), last.size());
106 return result;
107}
108
109void mkdir(zstring_view path, mode_t mode)
110{
111#ifdef _WIN32
112 (void)&mode; // Suppress C4100 VC++ warning
113 if ((path == "/") || path.ends_with(':') || path.ends_with(":/")) {
114 return;
115 }
116 int result = _wmkdir(utf8to16(getNativePath(string(path))).c_str());
117#else
118 int result = ::mkdir(path.c_str(), mode);
119#endif
120 if (result && (errno != EEXIST)) {
121 throw FileException("Error creating dir ", path);
122 }
123}
124
125static bool isUNCPath(string_view path)
126{
127#ifdef _WIN32
128 return path.starts_with("//") || path.starts_with("\\\\");
129#else
130 (void)path;
131 return false;
132#endif
133}
134
135void mkdirp(string path)
136{
137 if (path.empty()) {
138 return;
139 }
140
141 // We may receive platform-specific paths here, so conventionalize early
142 path = getConventionalPath(std::move(path));
143
144 // If the directory already exists, don't try to recreate it
145 if (isDirectory(path))
146 return;
147
148 // If the path is a UNC path (e.g. \\server\share) then the first two paths in the loop below will be \ and \\server
149 // If the path is an absolute path (e.g. c:\foo\bar) then the first path in the loop will be C:
150 // None of those are valid directory paths, so we skip over them and don't call mkdir.
151 // Relative paths are fine, since each segment in the path is significant.
152 int skip = isUNCPath(path) ? 2 :
153 isAbsolutePath(path) ? 1 : 0;
154 string::size_type pos = 0;
155 do {
156 pos = path.find_first_of('/', pos + 1);
157 if (skip) {
158 skip--;
159 continue;
160 }
161 mkdir(path.substr(0, pos), 0755);
162 } while (pos != string::npos);
163
164 if (!isDirectory(path)) {
165 throw FileException("Error creating dir ", path);
166 }
167}
168
170{
171#ifdef _WIN32
172 return _wunlink(utf8to16(path).c_str());
173#else
174 return ::unlink(path.c_str());
175#endif
176}
177
179{
180#ifdef _WIN32
181 return _wrmdir(utf8to16(path).c_str());
182#else
183 return ::rmdir(path.c_str());
184#endif
185}
186
187#ifdef _WIN32
189{
190 std::wstring pathW = utf8to16(path);
191
192 SHFILEOPSTRUCTW rmdirFileOp;
193 rmdirFileOp.hwnd = nullptr;
194 rmdirFileOp.wFunc = FO_DELETE;
195 rmdirFileOp.pFrom = pathW.c_str();
196 rmdirFileOp.pTo = nullptr;
197 rmdirFileOp.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI;
198 rmdirFileOp.fAnyOperationsAborted = FALSE;
199 rmdirFileOp.hNameMappings = nullptr;
200 rmdirFileOp.lpszProgressTitle = nullptr;
201
202 return SHFileOperationW(&rmdirFileOp);
203}
204#elif HAVE_NFTW
205static int deleteRecursive_cb(const char* fpath, const struct stat* /*sb*/,
206 int /*typeflag*/, struct FTW* /*ftwbuf*/)
207{
208 return remove(fpath);
209}
211{
212 return nftw(path.c_str(), deleteRecursive_cb, 64, FTW_DEPTH | FTW_PHYS);
213}
214#else
215// This is a platform independent version of deleteRecursive() (it builds on
216// top of helper routines that _are_ platform specific). Though I still prefer
217// the two platform specific deleteRecursive() routines above because they are
218// likely more optimized and likely contain less bugs than this version (e.g.
219// we're walking over the entries in a directory while simultaneously deleting
220// entries in that same directory. Although this seems to work fine, I'm not
221// 100% sure our ReadDir 'emulation code' for windows covers all corner cases.
222// While the windows version above very likely does handle everything).
223int deleteRecursive(const std::string& path)
224{
225 if (isDirectory(path)) {
226 {
227 ReadDir dir(path);
228 while (dirent* d = dir.getEntry()) {
229 int err = deleteRecursive(d->d_name);
230 if (err) return err;
231 }
232 }
233 return rmdir(path);
234 } else {
235 return unlink(path);
236 }
237}
238#endif
239
241{
242 // Mode must contain a 'b' character. On unix this doesn't make any
243 // difference. But on windows this is required to open the file
244 // in binary mode.
245 assert(mode.find('b') != std::string::npos);
246#ifdef _WIN32
247 return FILE_t(_wfopen(utf8to16(filename).c_str(),
248 utf8to16(mode).c_str()));
249#else
250 return FILE_t(fopen(filename.c_str(), mode.c_str()));
251#endif
252}
253
254void openOfStream(std::ofstream& stream, zstring_view filename)
255{
256#if defined _WIN32 && defined _MSC_VER
257 // MinGW 3.x doesn't support ofstream.open(wchar_t*)
258 // TODO - this means that unicode text may not work right here
259 stream.open(utf8to16(filename).c_str());
260#else
261 stream.open(filename.c_str());
262#endif
263}
264
265void openOfStream(std::ofstream& stream, zstring_view filename,
266 std::ios_base::openmode mode)
267{
268#if defined _WIN32 && defined _MSC_VER
269 // MinGW 3.x doesn't support ofstream.open(wchar_t*)
270 // TODO - this means that unicode text may not work right here
271 stream.open(utf8to16(filename).c_str(), mode);
272#else
273 stream.open(filename.c_str(), mode);
274#endif
275}
276
277string_view getFilename(string_view path)
278{
279 if (auto pos = path.rfind('/'); pos != string_view::npos) {
280 return path.substr(pos + 1);
281 }
282 return path;
283}
284
285string_view getDirName(string_view path)
286{
287 if (auto pos = path.rfind('/'); pos != string_view::npos) {
288 return path.substr(0, pos + 1);
289 }
290 return {};
291}
292
293string_view getExtension(string_view path)
294{
295 string_view filename = getFilename(path);
296 if (auto pos = filename.rfind('.'); pos != string_view::npos) {
297 return filename.substr(pos);
298 }
299 return {};
300}
301
302string_view stripExtension(string_view path)
303{
304 if (auto pos = path.rfind('.'); pos != string_view::npos) {
305 return path.substr(0, pos);
306 }
307 return path;
308}
309
310string join(string_view part1, string_view part2)
311{
312 if (part1.empty() || isAbsolutePath(part2)) {
313 return string(part2);
314 }
315 if (part1.back() == '/') {
316 return strCat(part1, part2);
317 }
318 return strCat(part1, '/', part2);
319}
320string join(string_view part1, string_view part2, string_view part3)
321{
322 return join(part1, join(part2, part3));
323}
324
325string join(string_view part1, string_view part2,
326 string_view part3, string_view part4)
327{
328 return join(part1, join(part2, join(part3, part4)));
329}
330
331#ifdef _WIN32
332string getNativePath(string path)
333{
334 ranges::replace(path, '/', '\\');
335 return path;
336}
337
338string getConventionalPath(string path)
339{
340 ranges::replace(path, '\\', '/');
341 return path;
342}
343#endif
344
346{
347#ifdef _WIN32
348 wchar_t bufW[MAXPATHLEN];
349 wchar_t* result = _wgetcwd(bufW, MAXPATHLEN);
350 if (!result) {
351 throw FileException("Couldn't get current working directory.");
352 }
353 return utf16to8(result);
354#else
355 std::array<char, MAXPATHLEN> buf;
356 char* result = getcwd(buf.data(), buf.size());
357 if (!result) {
358 throw FileException("Couldn't get current working directory.");
359 }
360 return buf.data();
361#endif
362}
363
364string getAbsolutePath(string_view path)
365{
366 // In rare cases getCurrentWorkingDirectory() can throw,
367 // so only call it when really necessary.
368 if (isAbsolutePath(path)) {
369 return string(path);
370 }
371 return join(getCurrentWorkingDirectory(), path);
372}
373
374bool isAbsolutePath(string_view path)
375{
376 if (isUNCPath(path)) return true;
377#ifdef _WIN32
378 if ((path.size() >= 3) && (path[1] == ':') && (path[2] == one_of('/', '\\'))) {
379 char drive = tolower(path[0]);
380 if (('a' <= drive) && (drive <= 'z')) {
381 return true;
382 }
383 }
384#endif
385 return !path.empty() && (path[0] == '/');
386}
387
388string getUserHomeDir(string_view username)
389{
390#ifdef _WIN32
391 (void)(&username); // ignore parameter, avoid warning
392
393 wchar_t bufW[MAXPATHLEN + 1];
394 if (!SHGetSpecialFolderPathW(nullptr, bufW, CSIDL_PERSONAL, TRUE)) {
395 throw FatalError(
396 "SHGetSpecialFolderPathW failed: ", GetLastError());
397 }
398
399 return getConventionalPath(utf16to8(bufW));
400#else
401 const char* dir = nullptr;
402 struct passwd* pw = nullptr;
403 if (username.empty()) {
404 dir = getenv("HOME");
405 if (!dir) {
406 pw = getpwuid(getuid());
407 }
408 } else {
409 pw = getpwnam(string(username).c_str());
410 }
411 if (pw) {
412 dir = pw->pw_dir;
413 }
414 return dir ? dir : string{};
415#endif
416}
417
418const string& getUserOpenMSXDir()
419{
420 static const string OPENMSX_DIR = []() -> string {
421 if (const char* home = getenv("OPENMSX_HOME")) {
422 return home;
423 }
424#ifdef _WIN32
425 return expandTilde("~/openMSX");
426#elif PLATFORM_ANDROID
427 // TODO: do something to query whether the storage is available
428 // via SDL_AndroidGetExternalStorageState
429 return strCat(SDL_AndroidGetExternalStoragePath(), "/openMSX");
430#else
431 return expandTilde("~/.openMSX");
432#endif
433 }();
434 return OPENMSX_DIR;
435}
436
437const string& getUserDataDir()
438{
439 static std::optional<string> result;
440 if (!result) {
441 const char* const NAME = "OPENMSX_USER_DATA";
442 char* value = getenv(NAME);
443 result = value ? value : getUserOpenMSXDir() + "/share";
444 }
445 return *result;
446}
447
448const string& getSystemDataDir()
449{
450 static std::optional<string> result;
451 if (!result) result = []() -> string {
452 if (char* value = getenv("OPENMSX_SYSTEM_DATA")) {
453 return value;
454 }
455#ifdef _WIN32
456 wchar_t bufW[MAXPATHLEN + 1];
457 int res = GetModuleFileNameW(nullptr, bufW, DWORD(std::size(bufW)));
458 if (!res) {
459 throw FatalError(
460 "Cannot detect openMSX directory. GetModuleFileNameW failed: ",
461 GetLastError());
462 }
463
464 string filename = utf16to8(bufW);
465 auto pos = filename.find_last_of('\\');
466 if (pos == string::npos) {
467 throw FatalError("openMSX is not in directory!?");
468 }
469 return getConventionalPath(filename.substr(0, pos)) + "/share";
470#elif defined(__APPLE__)
471 return findResourceDir("share");
472#elif PLATFORM_ANDROID
473 return getAbsolutePath("openmsx_system");
474#else
475 // defined in build-info.hh (default /opt/openMSX/share)
476 return DATADIR;
477#endif
478 }();
479 return *result;
480}
481
482const string& getSystemDocDir()
483{
484 static std::optional<string> result;
485 if (!result) result = []() -> string {
486#ifdef _WIN32
487 wchar_t bufW[MAXPATHLEN + 1];
488 int res = GetModuleFileNameW(nullptr, bufW, DWORD(std::size(bufW)));
489 if (!res) {
490 throw FatalError(
491 "Cannot detect openMSX directory. GetModuleFileNameW failed: ",
492 GetLastError());
493 }
494
495 string filename = utf16to8(bufW);
496 auto pos = filename.find_last_of('\\');
497 if (pos == string::npos) {
498 throw FatalError("openMSX is not in directory!?");
499 }
500 return getConventionalPath(filename.substr(0, pos)) + "/doc";
501#elif defined(__APPLE__)
502 return findResourceDir("doc");
503#elif PLATFORM_ANDROID
504 return getAbsolutePath("openmsx_system"); // TODO: currently no docs are installed on Android
505#else
506 // defined in build-info.hh (default /opt/openMSX/doc)
507 return DOCDIR;
508#endif
509 }();
510 return *result;
511}
512
513#ifdef _WIN32
514static bool driveExists(char driveLetter)
515{
516 char buf[] = { driveLetter, ':', 0 };
517 return GetFileAttributesA(buf) != INVALID_FILE_ATTRIBUTES;
518}
519#endif
520
521#ifdef _WIN32
522string expandCurrentDirFromDrive(string path)
523{
524 string result = path;
525 if (((path.size() == 2) && (path[1] == ':')) ||
526 ((path.size() >= 3) && (path[1] == ':') && (path[2] != '/'))) {
527 // get current directory for this drive
528 unsigned char drive = tolower(path[0]);
529 if (('a' <= drive) && (drive <= 'z')) {
530 wchar_t bufW[MAXPATHLEN + 1];
531 if (driveExists(drive) &&
532 _wgetdcwd(drive - 'a' + 1, bufW, MAXPATHLEN)) {
533 result = getConventionalPath(utf16to8(bufW));
534 if (result.back() != '/') {
535 result += '/';
536 }
537 if (path.size() > 2) {
538 string_view tmp = path.substr(2);
539 result.append(tmp.data(), tmp.size());
540 }
541 }
542 }
543 }
544 return result;
545}
546#endif
547
548std::optional<Stat> getStat(zstring_view filename)
549{
550 std::optional<Stat> st;
551 st.emplace(); // allocate relatively large 'struct stat' in the return slot
552
553#ifdef _WIN32
554 std::string filename2(filename);
555 // workaround for VC++: strip trailing slashes (but keep it if it's the
556 // only character in the path)
557 if (auto pos = filename2.find_last_not_of('/'); pos != string::npos) {
558 filename2.resize(pos + 1);
559 } else {
560 // string was either empty or a (sequence of) '/' character(s)
561 if (!filename2.empty()) filename2.resize(1);
562 }
563 if (_wstat(utf8to16(filename2).c_str(), &*st)) {
564 st.reset();
565 }
566#else
567 if (stat(filename.c_str(), &*st)) {
568 st.reset();
569 }
570#endif
571
572 return st; // we count on NRVO to eliminate memcpy of 'struct stat'
573}
574
575bool isRegularFile(const Stat& st)
576{
577 return S_ISREG(st.st_mode);
578}
580{
581 auto st = getStat(filename);
582 return st && isRegularFile(*st);
583}
584
585bool isDirectory(const Stat& st)
586{
587 return S_ISDIR(st.st_mode);
588}
589
591{
592 auto st = getStat(directory);
593 return st && isDirectory(*st);
594}
595
596bool exists(zstring_view filename)
597{
598 return static_cast<bool>(getStat(filename));
599}
600
601static unsigned getNextNum(dirent* d, string_view prefix, string_view extension,
602 unsigned nofDigits)
603{
604 auto extensionLen = extension.size();
605 auto prefixLen = prefix.size();
606 string_view name(d->d_name);
607
608 if ((name.size() != (prefixLen + nofDigits + extensionLen)) ||
609 (name.substr(0, prefixLen) != prefix) ||
610 (name.substr(prefixLen + nofDigits, extensionLen) != extension)) {
611 return 0;
612 }
613 if (auto n = StringOp::stringToBase<10, unsigned>(name.substr(prefixLen, nofDigits))) {
614 return *n;
615 }
616 return 0;
617}
618
620 string_view directory, string_view prefix, string_view extension, bool addSeparator)
621{
622 std::string newPrefix;
623 if (addSeparator) {
624 newPrefix = strCat(prefix, ((prefix.find(' ') != std::string_view::npos) ? ' ' : '_'));
625 prefix = newPrefix;
626 }
627
628 const unsigned nofDigits = 4;
629
630 unsigned max_num = 0;
631
632 string dirName = join(getUserOpenMSXDir(), directory);
633 try {
634 mkdirp(dirName);
635 } catch (FileException&) {
636 // ignore
637 }
638
639 ReadDir dir(dirName);
640 while (auto* d = dir.getEntry()) {
641 max_num = std::max(max_num, getNextNum(d, prefix, extension, nofDigits));
642 }
643
644 std::ostringstream os;
645 os << FileOperations::join(dirName, prefix);
646 os.width(nofDigits);
647 os.fill('0');
648 os << (max_num + 1) << extension;
649 return os.str();
650}
651
653 string_view argument, string_view directory,
654 string_view prefix, string_view extension)
655{
656 if (argument.empty()) {
657 // directory is also created when needed
658 return getNextNumberedFileName(directory, prefix, extension);
659 }
660
661 string filename(argument);
662 if (getDirName(filename).empty()) {
663 // no dir given, use standard dir (and create it)
664 string dir = strCat(getUserOpenMSXDir(), '/', directory);
665 mkdirp(dir);
666 filename = strCat(dir, '/', filename);
667 } else {
668 filename = expandTilde(std::move(filename));
669 }
670
671 if (!filename.ends_with(extension) && !exists(filename)) {
672 // Expected extension not already given, append it. But only
673 // when the filename without extension doesn't already exist.
674 // Without this exception stuff like 'soundlog start /dev/null'
675 // reports an error " ... error opening file /dev/null.wav."
676 filename.append(extension.data(), extension.size());
677 }
678 return filename;
679}
680
682{
683#ifdef _WIN32
684 DWORD len = GetTempPathW(0, nullptr);
685 if (len) {
686 VLA(wchar_t, bufW, (len + 1));
687 len = GetTempPathW(len, bufW.data());
688 if (len) {
689 // Strip last backslash
690 if (bufW[len - 1] == L'\\') {
691 bufW[len - 1] = L'\0';
692 }
693 return utf16to8(bufW.data());
694 }
695 }
696 throw FatalError("GetTempPathW failed: ", GetLastError());
697#elif PLATFORM_ANDROID
698 string result = getSystemDataDir() + "/tmp";
699 return result;
700#else
701 const char* result = nullptr;
702 if (!result) result = getenv("TMPDIR");
703 if (!result) result = getenv("TMP");
704 if (!result) result = getenv("TEMP");
705 if (!result) {
706 result = "/tmp";
707 }
708 return result;
709#endif
710}
711
712FILE_t openUniqueFile(const std::string& directory, std::string& filename)
713{
714#ifdef _WIN32
715 std::wstring directoryW = utf8to16(directory);
716 wchar_t filenameW[MAX_PATH];
717 if (!GetTempFileNameW(directoryW.c_str(), L"msx", 0, filenameW)) {
718 throw FileException("GetTempFileNameW failed: ", GetLastError());
719 }
720 filename = utf16to8(filenameW);
721 return FILE_t(_wfopen(filenameW, L"wb"));
722#else
723 filename = directory + "/XXXXXX";
724 auto oldMask = umask(S_IRWXO | S_IRWXG);
725 int fd = mkstemp(const_cast<char*>(filename.c_str()));
726 umask(oldMask);
727 if (fd == -1) {
728 throw FileException("Couldnt get temp file name");
729 }
730 return FILE_t(fdopen(fd, "wb"));
731#endif
732}
733
734} // namespace openmsx::FileOperations
#define MAXPATHLEN
Simple wrapper around opendir() / readdir() / closedir() functions.
Definition ReadDir.hh:16
struct dirent * getEntry()
Get directory entry for next file.
Definition ReadDir.cc:17
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
constexpr bool ends_with(std::string_view sv) const
constexpr const char * c_str() const
constexpr auto find(char c, size_type pos=0) const
string parseCommandFileArgument(string_view argument, string_view directory, string_view prefix, string_view extension)
Helper function for parsing filename arguments in Tcl commands.
std::string findResourceDir(const std::string &resourceDirName)
bool exists(zstring_view filename)
Does this file (directory) exists?
FILE_t openUniqueFile(const std::string &directory, std::string &filename)
Open a new file with a unique name in the provided directory.
void mkdir(zstring_view path, mode_t mode)
Create the specified directory.
string expandTilde(string path)
Expand the '~' character to the users home directory.
string_view getDirName(string_view path)
Returns the directory portion of a path.
string getNextNumberedFileName(string_view directory, string_view prefix, string_view extension, bool addSeparator)
Gets the next numbered file name with the specified prefix in the specified directory,...
const std::string & getNativePath(const std::string &path)
Returns the path in native path-delimiter.
string getCurrentWorkingDirectory()
Returns the current working directory.
const std::string & expandCurrentDirFromDrive(const std::string &path)
Get the current directory of the specified drive Linux: return the given string unchanged.
string getUserHomeDir(string_view username)
Get user's home directory.
bool isRegularFile(const Stat &st)
string_view getExtension(string_view path)
Returns the extension portion of a path.
const string & getUserOpenMSXDir()
Get the openMSX dir in the user's home directory.
int rmdir(zstring_view path)
Call rmdir() in a platform-independent manner.
string getAbsolutePath(string_view path)
Transform given path into an absolute path.
string_view stripExtension(string_view path)
Returns the path without extension.
string getTempDir()
Get the name of the temp directory on the system.
int deleteRecursive(const std::string &path)
bool isDirectory(const Stat &st)
void openOfStream(std::ofstream &stream, zstring_view filename)
Open an ofstream in a platform-independent manner.
bool isAbsolutePath(string_view path)
Checks whether it's a absolute path or not.
string_view getFilename(string_view path)
Returns the file portion of a path name.
const string & getSystemDataDir()
Get system directory.
std::optional< Stat > getStat(zstring_view filename)
Call stat() and return the stat structure.
FILE_t openFile(zstring_view filename, zstring_view mode)
Call fopen() in a platform-independent manner.
const string & getSystemDocDir()
Get system doc directory.
bool needsTildeExpansion(std::string_view path)
Returns true iff expandTilde(s) would have an effect.
const std::string & getConventionalPath(const std::string &path)
Returns the path in conventional path-delimiter.
void mkdirp(string path)
Acts like the unix command "mkdir -p".
const string & getUserDataDir()
Get the openMSX data dir in the user's home directory.
std::unique_ptr< FILE, FClose > FILE_t
int unlink(zstring_view path)
Call unlink() in a platform-independent manner.
string join(string_view part1, string_view part2)
Join two paths.
constexpr void replace(ForwardRange &&range, const T &old_value, const T &new_value)
Definition ranges.hh:293
octet_iterator utf16to8(u16bit_iterator start, u16bit_iterator end, octet_iterator result)
std::string strCat()
Definition strCat.hh:703
#define VLA(TYPE, NAME, LENGTH)
Definition vla.hh:12