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 "statp.hh"
49#include "unistdp.hh"
50#include "one_of.hh"
51#include "ranges.hh"
52#include "strCat.hh"
53#include "build-info.hh"
54#include <algorithm>
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 string buf;
351 if (result) {
352 buf = utf16to8(result);
353 }
354#else
355 char buf[MAXPATHLEN];
356 char* result = getcwd(buf, MAXPATHLEN);
357#endif
358 if (!result) {
359 throw FileException("Couldn't get current working directory.");
360 }
361 return buf;
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 findShareDir();
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
482#ifdef _WIN32
483static bool driveExists(char driveLetter)
484{
485 char buf[] = { driveLetter, ':', 0 };
486 return GetFileAttributesA(buf) != INVALID_FILE_ATTRIBUTES;
487}
488#endif
489
490#ifdef _WIN32
491string expandCurrentDirFromDrive(string path)
492{
493 string result = path;
494 if (((path.size() == 2) && (path[1] == ':')) ||
495 ((path.size() >= 3) && (path[1] == ':') && (path[2] != '/'))) {
496 // get current directory for this drive
497 unsigned char drive = tolower(path[0]);
498 if (('a' <= drive) && (drive <= 'z')) {
499 wchar_t bufW[MAXPATHLEN + 1];
500 if (driveExists(drive) &&
501 _wgetdcwd(drive - 'a' + 1, bufW, MAXPATHLEN)) {
502 result = getConventionalPath(utf16to8(bufW));
503 if (result.back() != '/') {
504 result += '/';
505 }
506 if (path.size() > 2) {
507 string_view tmp = path.substr(2);
508 result.append(tmp.data(), tmp.size());
509 }
510 }
511 }
512 }
513 return result;
514}
515#endif
516
518{
519#ifdef _WIN32
520 std::string filename2(filename);
521 // workaround for VC++: strip trailing slashes (but keep it if it's the
522 // only character in the path)
523 if (auto pos = filename2.find_last_not_of('/'); pos != string::npos) {
524 filename2.resize(pos + 1);
525 } else {
526 // string was either empty or a (sequence of) '/' character(s)
527 if (!filename2.empty()) filename2.resize(1);
528 }
529 return _wstat(utf8to16(filename2).c_str(), &st) == 0;
530#else
531 return stat(filename.c_str(), &st) == 0;
532#endif
533}
534
535bool isRegularFile(const Stat& st)
536{
537 return S_ISREG(st.st_mode);
538}
540{
541 Stat st;
542 return getStat(filename, st) && isRegularFile(st);
543}
544
545bool isDirectory(const Stat& st)
546{
547 return S_ISDIR(st.st_mode);
548}
549
551{
552 Stat st;
553 return getStat(directory, st) && isDirectory(st);
554}
555
557{
558 Stat st; // dummy
559 return getStat(filename, st);
560}
561
562static unsigned getNextNum(dirent* d, string_view prefix, string_view extension,
563 unsigned nofdigits)
564{
565 auto extensionLen = extension.size();
566 auto prefixLen = prefix.size();
567 string_view name(d->d_name);
568
569 if ((name.size() != (prefixLen + nofdigits + extensionLen)) ||
570 (name.substr(0, prefixLen) != prefix) ||
571 (name.substr(prefixLen + nofdigits, extensionLen) != extension)) {
572 return 0;
573 }
574 if (auto n = StringOp::stringToBase<10, unsigned>(name.substr(prefixLen, nofdigits))) {
575 return *n;
576 }
577 return 0;
578}
579
581 string_view directory, string_view prefix, string_view extension)
582{
583 const unsigned nofdigits = 4;
584
585 unsigned max_num = 0;
586
587 string dirName = strCat(getUserOpenMSXDir(), '/', directory);
588 try {
589 mkdirp(dirName);
590 } catch (FileException&) {
591 // ignore
592 }
593
594 ReadDir dir(dirName);
595 while (auto* d = dir.getEntry()) {
596 max_num = std::max(max_num, getNextNum(d, prefix, extension, nofdigits));
597 }
598
599 std::ostringstream os;
600 os << dirName << '/' << prefix;
601 os.width(nofdigits);
602 os.fill('0');
603 os << (max_num + 1) << extension;
604 return os.str();
605}
606
608 string_view argument, string_view directory,
609 string_view prefix, string_view extension)
610{
611 if (argument.empty()) {
612 // directory is also created when needed
613 return getNextNumberedFileName(directory, prefix, extension);
614 }
615
616 string filename(argument);
617 if (getDirName(filename).empty()) {
618 // no dir given, use standard dir (and create it)
619 string dir = strCat(getUserOpenMSXDir(), '/', directory);
620 mkdirp(dir);
621 filename = strCat(dir, '/', filename);
622 } else {
623 filename = expandTilde(std::move(filename));
624 }
625
626 if (!filename.ends_with(extension) && !exists(filename)) {
627 // Expected extension not already given, append it. But only
628 // when the filename without extension doesn't already exist.
629 // Without this exception stuff like 'soundlog start /dev/null'
630 // reports an error " ... error opening file /dev/null.wav."
631 filename.append(extension.data(), extension.size());
632 }
633 return filename;
634}
635
637{
638#ifdef _WIN32
639 DWORD len = GetTempPathW(0, nullptr);
640 if (len) {
641 VLA(wchar_t, bufW, (len+1));
642 len = GetTempPathW(len, bufW);
643 if (len) {
644 // Strip last backslash
645 if (bufW[len-1] == L'\\') {
646 bufW[len-1] = L'\0';
647 }
648 return utf16to8(bufW);
649 }
650 }
651 throw FatalError("GetTempPathW failed: ", GetLastError());
652#elif PLATFORM_ANDROID
653 string result = getSystemDataDir() + "/tmp";
654 return result;
655#else
656 const char* result = nullptr;
657 if (!result) result = getenv("TMPDIR");
658 if (!result) result = getenv("TMP");
659 if (!result) result = getenv("TEMP");
660 if (!result) {
661 result = "/tmp";
662 }
663 return result;
664#endif
665}
666
667FILE_t openUniqueFile(const std::string& directory, std::string& filename)
668{
669#ifdef _WIN32
670 std::wstring directoryW = utf8to16(directory);
671 wchar_t filenameW[MAX_PATH];
672 if (!GetTempFileNameW(directoryW.c_str(), L"msx", 0, filenameW)) {
673 throw FileException("GetTempFileNameW failed: ", GetLastError());
674 }
675 filename = utf16to8(filenameW);
676 return FILE_t(_wfopen(filenameW, L"wb"));
677#else
678 filename = directory + "/XXXXXX";
679 auto oldMask = umask(S_IRWXO | S_IRWXG);
680 int fd = mkstemp(const_cast<char*>(filename.c_str()));
681 umask(oldMask);
682 if (fd == -1) {
683 throw FileException("Couldnt get temp file name");
684 }
685 return FILE_t(fdopen(fd, "wb"));
686#endif
687}
688
689} // namespace openmsx::FileOperations
#define MAXPATHLEN
Definition: one_of.hh:7
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.
Definition: zstring_view.hh:22
constexpr bool ends_with(std::string_view sv) const
Definition: zstring_view.hh:69
constexpr const char * c_str() const
Definition: zstring_view.hh:49
constexpr auto find(char c, size_type pos=0) const
Definition: zstring_view.hh:51
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:283
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 findShareDir()
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.
void openofstream(std::ofstream &stream, zstring_view filename)
Open an ofstream in a platform-independent manner.
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.
const std::string & getNativePath(const std::string &path)
Returns the path in native path-delimiter.
string getCurrentWorkingDirectory()
Returns the current working directory.
bool getStat(zstring_view filename, Stat &st)
Call stat() and return the stat structure.
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.
bool isDirectory(const Stat &st)
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.
FILE_t openFile(zstring_view filename, zstring_view mode)
Call fopen() in a platform-independent manner.
bool needsTildeExpansion(std::string_view path)
Returns true iff expandTilde(s) would have an effect.
int deleteRecursive(zstring_view path)
Recursively delete a file or directory and (in case of a directory) all its sub-components.
const std::string & getConventionalPath(const std::string &path)
Returns the path in conventional path-delimiter.
string getNextNumberedFileName(string_view directory, string_view prefix, string_view extension)
Gets the next numbered file name with the specified prefix in the specified directory,...
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 const char *const filename
auto remove(ForwardRange &&range, const T &value)
Definition: ranges.hh:232
constexpr void replace(ForwardRange &&range, const T &old_value, const T &new_value)
Definition: ranges.hh:244
size_t size(std::string_view utf8)
u16bit_iterator utf8to16(octet_iterator start, octet_iterator end, u16bit_iterator result)
octet_iterator utf16to8(u16bit_iterator start, u16bit_iterator end, octet_iterator result)
std::string strCat(Ts &&...ts)
Definition: strCat.hh:549
#define VLA(TYPE, NAME, LENGTH)
Definition: vla.hh:10