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 "systemfuncs.hh"
24 
25 #if HAVE_NFTW
26 #include <ftw.h>
27 #endif
28 
29 #if defined(PATH_MAX)
30 #define MAXPATHLEN PATH_MAX
31 #elif defined(MAX_PATH)
32 #define MAXPATHLEN MAX_PATH
33 #else
34 #define MAXPATHLEN 4096
35 #endif
36 
37 
38 #ifdef __APPLE__
39 #include <Carbon/Carbon.h>
40 #endif
41 
42 #include "openmsx.hh"
43 #include "ReadDir.hh"
44 #include "FileOperations.hh"
45 #include "FileException.hh"
46 #include "StringOp.hh"
47 #include "statp.hh"
48 #include "unistdp.hh"
49 #include "countof.hh"
50 #include "build-info.hh"
51 #include "AndroidApiWrapper.hh"
52 #include <sstream>
53 #include <cerrno>
54 #include <cstdlib>
55 #include <cassert>
56 
57 #ifndef _MSC_VER
58 #include <dirent.h>
59 #endif
60 
61 using std::string;
62 
63 #ifdef _WIN32
64 using namespace utf8;
65 #endif
66 
67 namespace openmsx {
68 
69 namespace FileOperations {
70 
71 #ifdef __APPLE__
72 
73 static std::string findShareDir()
74 {
75  // Find bundle location:
76  // for an app folder, this is the outer directory,
77  // for an unbundled executable, it is the executable itself.
78  ProcessSerialNumber psn;
79  if (GetCurrentProcess(&psn) != noErr) {
80  throw FatalError("Failed to get process serial number");
81  }
82  FSRef location;
83  if (GetProcessBundleLocation(&psn, &location) != noErr) {
84  throw FatalError("Failed to get process bundle location");
85  }
86  // Get info about the location.
87  FSCatalogInfo catalogInfo;
88  FSRef parentRef;
89  if (FSGetCatalogInfo(
90  &location, kFSCatInfoVolume | kFSCatInfoNodeFlags,
91  &catalogInfo, nullptr, nullptr, &parentRef
92  ) != noErr) {
93  throw FatalError("Failed to get info about bundle path");
94  }
95  // Get reference to root directory of the volume we are searching.
96  // We will need this later to know when to give up.
97  FSRef root;
98  if (FSGetVolumeInfo(
99  catalogInfo.volume, 0, nullptr, kFSVolInfoNone, nullptr, nullptr, &root
100  ) != noErr) {
101  throw FatalError("Failed to get reference to root directory");
102  }
103  // Make sure we are looking at a directory.
104  if (~catalogInfo.nodeFlags & kFSNodeIsDirectoryMask) {
105  // Location is not a directory, so it is the path to the executable.
106  location = parentRef;
107  }
108  while (true) {
109  // Iterate through the files in the directory.
110  FSIterator iterator;
111  if (FSOpenIterator(&location, kFSIterateFlat, &iterator) != noErr) {
112  throw FatalError("Failed to open iterator");
113  }
114  bool filesLeft = true; // iterator has files left for next call
115  while (filesLeft) {
116  // Get info about several files at a time.
117  const int MAX_SCANNED_FILES = 100;
118  ItemCount actualObjects;
119  FSRef refs[MAX_SCANNED_FILES];
120  FSCatalogInfo catalogInfos[MAX_SCANNED_FILES];
121  HFSUniStr255 names[MAX_SCANNED_FILES];
122  OSErr err = FSGetCatalogInfoBulk(
123  iterator,
124  MAX_SCANNED_FILES,
125  &actualObjects,
126  nullptr /*containerChanged*/,
127  kFSCatInfoNodeFlags,
128  catalogInfos,
129  refs,
130  nullptr /*specs*/,
131  names
132  );
133  if (err == errFSNoMoreItems) {
134  filesLeft = false;
135  } else if (err != noErr) {
136  throw FatalError("Catalog get failed");
137  }
138  for (ItemCount i = 0; i < actualObjects; i++) {
139  // We're only interested in subdirectories.
140  if (catalogInfos[i].nodeFlags & kFSNodeIsDirectoryMask) {
141  // Convert the name to a CFString.
142  CFStringRef name = CFStringCreateWithCharactersNoCopy(
143  kCFAllocatorDefault,
144  names[i].unicode,
145  names[i].length,
146  kCFAllocatorNull // do not deallocate character buffer
147  );
148  // Is this the directory we are looking for?
149  static const CFStringRef SHARE = CFSTR("share");
150  CFComparisonResult cmp = CFStringCompare(SHARE, name, 0);
151  CFRelease(name);
152  if (cmp == kCFCompareEqualTo) {
153  // Clean up.
154  OSErr closeErr = FSCloseIterator(iterator);
155  assert(closeErr == noErr); (void)closeErr;
156  // Get full path of directory.
157  UInt8 path[256];
158  if (FSRefMakePath(
159  &refs[i], path, sizeof(path)) != noErr
160  ) {
161  throw FatalError("Path too long");
162  }
163  return std::string(reinterpret_cast<char*>(path));
164  }
165  }
166  }
167  }
168  OSErr closeErr = FSCloseIterator(iterator);
169  assert(closeErr == noErr); (void)closeErr;
170  // Are we in the root yet?
171  if (FSCompareFSRefs(&location, &root) == noErr) {
172  throw FatalError("Could not find \"share\" directory anywhere");
173  }
174  // Go up one level.
175  if (FSGetCatalogInfo(
176  &location, kFSCatInfoNone, nullptr, nullptr, nullptr, &parentRef
177  ) != noErr
178  ) {
179  throw FatalError("Failed to get parent directory");
180  }
181  location = parentRef;
182  }
183 }
184 
185 #endif // __APPLE__
186 
188 {
189  if (path.empty() || path[0] != '~') {
190  return path.str();
191  }
192  auto pos = path.find_first_of('/');
193  string_ref user = ((path.size() == 1) || (pos == 1))
194  ? ""
195  : path.substr(1, (pos == string_ref::npos) ? pos : pos - 1);
196  string result = getUserHomeDir(user);
197  if (result.empty()) {
198  // failed to find homedir, return the path unchanged
199  return path.str();
200  }
201  if (pos == string_ref::npos) {
202  return result;
203  }
204  if (result.back() != '/') {
205  result += '/';
206  }
207  string_ref last = path.substr(pos + 1);
208  result.append(last.data(), last.size());
209  return result;
210 }
211 
212 void mkdir(const string& path, mode_t mode)
213 {
214 #ifdef _WIN32
215  (void)&mode; // Suppress C4100 VC++ warning
216  if ((path == "/") ||
217  StringOp::endsWith(path, ':') ||
218  StringOp::endsWith(path, ":/")) {
219  return;
220  }
221  int result = _wmkdir(utf8to16(getNativePath(path)).c_str());
222 #else
223  int result = ::mkdir(path.c_str(), mode);
224 #endif
225  if (result && (errno != EEXIST)) {
226  throw FileException("Error creating dir " + path);
227  }
228 }
229 
230 void mkdirp(string_ref path_)
231 {
232  if (path_.empty()) {
233  return;
234  }
235  string path = expandTilde(path_);
236 
237  string::size_type pos = 0;
238  do {
239  pos = path.find_first_of('/', pos + 1);
240  mkdir(path.substr(0, pos), 0755);
241  } while (pos != string::npos);
242 
243  if (!isDirectory(path)) {
244  throw FileException("Error creating dir " + path);
245  }
246 }
247 
248 int unlink(const std::string& path)
249 {
250 #ifdef _WIN32
251  return _wunlink(utf8to16(path).c_str());
252 #else
253  return ::unlink(path.c_str());
254 #endif
255 }
256 
257 int rmdir(const std::string& path)
258 {
259 #ifdef _WIN32
260  return _wrmdir(utf8to16(path).c_str());
261 #else
262  return ::rmdir(path.c_str());
263 #endif
264 }
265 
266 #ifdef _WIN32
267 int deleteRecursive(const std::string& path)
268 {
269  std::wstring pathW = utf8to16(path);
270 
271  SHFILEOPSTRUCTW rmdirFileop;
272  rmdirFileop.hwnd = nullptr;
273  rmdirFileop.wFunc = FO_DELETE;
274  rmdirFileop.pFrom = pathW.c_str();
275  rmdirFileop.pTo = nullptr;
276  rmdirFileop.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI;
277  rmdirFileop.fAnyOperationsAborted = FALSE;
278  rmdirFileop.hNameMappings = nullptr;
279  rmdirFileop.lpszProgressTitle = nullptr;
280 
281  return SHFileOperationW(&rmdirFileop);
282 }
283 #elif HAVE_NFTW
284 int deleteRecursive_cb(const char* fpath, const struct stat* /*sb*/,
285  int /*typeflag*/, struct FTW* /*ftwbuf*/)
286 {
287  return remove(fpath);
288 }
289 int deleteRecursive(const std::string& path)
290 {
291  return nftw(path.c_str(), deleteRecursive_cb, 64, FTW_DEPTH | FTW_PHYS);
292 }
293 #else
294 // This is a platform independent version of deleteRecursive() (it builds on
295 // top of helper routines that _are_ platform specific). Though I still prefer
296 // the two platform specific deleteRecursive() routines above because they are
297 // likely more optimized and likely contain less bugs than this version (e.g.
298 // we're walking over the entries in a directory while simultaneously deleting
299 // entries in that same directory. Although this seems to work fine, I'm not
300 // 100% sure our ReadDir 'emulation code' for windows covers all corner cases.
301 // While the windows version above very likely does handle everything).
302 int deleteRecursive(const std::string& path)
303 {
304  if (isDirectory(path)) {
305  {
306  ReadDir dir(path);
307  while (dirent* d = dir.getEntry()) {
308  int err = deleteRecursive(d->d_name);
309  if (err) return err;
310  }
311  }
312  return rmdir(path);
313  } else {
314  return unlink(path);
315  }
316 }
317 #endif
318 
319 FILE* openFile(const std::string& filename, const std::string& mode)
320 {
321  // Mode must contain a 'b' character. On unix this doesn't make any
322  // difference. But on windows this is required to open the file
323  // in binary mode.
324  assert(mode.find('b') != std::string::npos);
325 #ifdef _WIN32
326  return _wfopen(
327  utf8to16(filename).c_str(),
328  utf8to16(mode).c_str());
329 #else
330  return fopen(filename.c_str(), mode.c_str());
331 #endif
332 }
333 
334 void openofstream(std::ofstream& stream, const std::string& filename)
335 {
336 #if defined _WIN32 && defined _MSC_VER
337  // MinGW 3.x doesn't support ofstream.open(wchar_t*)
338  // TODO - this means that unicode text may not work right here
339  stream.open(utf8to16(filename).c_str());
340 #else
341  stream.open(filename.c_str());
342 #endif
343 }
344 
345 void openofstream(std::ofstream& stream, const std::string& filename,
346  std::ios_base::openmode mode)
347 {
348 #if defined _WIN32 && defined _MSC_VER
349  // MinGW 3.x doesn't support ofstream.open(wchar_t*)
350  // TODO - this means that unicode text may not work right here
351  stream.open(utf8to16(filename).c_str(), mode);
352 #else
353  stream.open(filename.c_str(), mode);
354 #endif
355 }
356 
358 {
359  auto pos = path.rfind('/');
360  if (pos == string_ref::npos) {
361  return path;
362  } else {
363  return path.substr(pos + 1);
364  }
365 }
366 
368 {
369  auto pos = path.rfind('/');
370  if (pos == string_ref::npos) {
371  return "";
372  } else {
373  return path.substr(0, pos + 1);
374  }
375 }
376 
378 {
379  string_ref filename = getFilename(path);
380  auto pos = filename.rfind('.');
381  if (pos == string_ref::npos) {
382  return "";
383  } else {
384  return filename.substr(pos + 1);
385  }
386 }
387 
389 {
390  auto pos = path.rfind('.');
391  if (pos == string_ref::npos) {
392  return path;
393  } else {
394  return path.substr(0, pos);
395  }
396 }
397 
398 string join(string_ref part1, string_ref part2)
399 {
400  if (part1.empty() || isAbsolutePath(part2)) {
401  return part2.str();
402  }
403  if (part1.back() == '/') {
404  return part1 + part2;
405  }
406  return part1 + '/' + part2;
407 }
408 string join(string_ref part1, string_ref part2, string_ref part3)
409 {
410  return join(part1, join(part2, part3));
411 }
412 
413 string join(string_ref part1, string_ref part2,
414  string_ref part3, string_ref part4)
415 {
416  return join(part1, join(part2, join(part3, part4)));
417 }
418 
420 {
421  string result = path.str();
422 #ifdef _WIN32
423  replace(result.begin(), result.end(), '/', '\\');
424 #endif
425  return result;
426 }
427 
429 {
430  string result = path.str();
431 #ifdef _WIN32
432  replace(result.begin(), result.end(), '\\', '/');
433 #endif
434  return result;
435 }
436 
438 {
439 #ifdef _WIN32
440  wchar_t bufW[MAXPATHLEN];
441  wchar_t* result = _wgetcwd(bufW, MAXPATHLEN);
442  string buf;
443  if (result) {
444  buf = utf16to8(result);
445  }
446 #else
447  char buf[MAXPATHLEN];
448  char* result = getcwd(buf, MAXPATHLEN);
449 #endif
450  if (!result) {
451  throw FileException("Couldn't get current working directory.");
452  }
453  return buf;
454 }
455 
457 {
458  // In rare cases getCurrentWorkingDirectory() can throw,
459  // so only call it when really necessary.
460  if (isAbsolutePath(path)) {
461  return path.str();
462  }
463  string currentDir = getCurrentWorkingDirectory();
464  return join(currentDir, path);
465 }
466 
468 {
469 #ifdef _WIN32
470  if ((path.size() >= 3) && (path[1] == ':') && (path[2] == '/')) {
471  char drive = tolower(path[0]);
472  if (('a' <= drive) && (drive <= 'z')) {
473  return true;
474  }
475  }
476 #endif
477  return !path.empty() && (path[0] == '/');
478 }
479 
480 string getUserHomeDir(string_ref username)
481 {
482 #ifdef _WIN32
483  (void)(&username); // ignore parameter, avoid warning
484 
485  wchar_t bufW[MAXPATHLEN + 1];
486  if (!SHGetSpecialFolderPathW(nullptr, bufW, CSIDL_PERSONAL, TRUE)) {
487  throw FatalError(StringOp::Builder() <<
488  "SHGetSpecialFolderPathW failed: " << GetLastError());
489  }
490 
491  return getConventionalPath(utf16to8(bufW));
492 
493 #elif PLATFORM_GP2X
494  return ""; // TODO figure out why stuff below doesn't work
495  // We cannot use generic implementation below, because for some
496  // reason getpwuid() and getpwnam() cannot be used in statically
497  // linked applications.
498  const char* dir = getenv("HOME");
499  if (!dir) {
500  dir = "/root";
501  }
502  return dir;
503 #else
504  const char* dir = nullptr;
505  struct passwd* pw = nullptr;
506  if (username.empty()) {
507  dir = getenv("HOME");
508  if (!dir) {
509  pw = getpwuid(getuid());
510  }
511  } else {
512  pw = getpwnam(username.str().c_str());
513  }
514  if (pw) {
515  dir = pw->pw_dir;
516  }
517  return dir ? dir : "";
518 #endif
519 }
520 
521 const string& getUserOpenMSXDir()
522 {
523 #ifdef _WIN32
524  static const string OPENMSX_DIR = expandTilde("~/openMSX");
525 #elif PLATFORM_ANDROID
526  static const string OPENMSX_DIR = AndroidApiWrapper::getStorageDirectory() + "/openMSX";
527 #else
528  static const string OPENMSX_DIR = expandTilde("~/.openMSX");
529 #endif
530  return OPENMSX_DIR;
531 }
532 
534 {
535  const char* const NAME = "OPENMSX_USER_DATA";
536  char* value = getenv(NAME);
537  return value ? value : getUserOpenMSXDir() + "/share";
538 }
539 
541 {
542  const char* const NAME = "OPENMSX_SYSTEM_DATA";
543  if (char* value = getenv(NAME)) {
544  return value;
545  }
546 
547  string newValue;
548 #ifdef _WIN32
549  wchar_t bufW[MAXPATHLEN + 1];
550  int res = GetModuleFileNameW(nullptr, bufW, countof(bufW));
551  if (!res) {
552  throw FatalError(StringOp::Builder() <<
553  "Cannot detect openMSX directory. GetModuleFileNameW failed: " <<
554  GetLastError());
555  }
556 
557  string filename = utf16to8(bufW);
558  auto pos = filename.find_last_of('\\');
559  if (pos == string::npos) {
560  throw FatalError("openMSX is not in directory!?");
561  }
562  newValue = getConventionalPath(filename.substr(0, pos)) + "/share";
563 #elif defined(__APPLE__)
564  newValue = findShareDir();
565 #elif PLATFORM_ANDROID
566  newValue = getAbsolutePath("openmsx_system");
567  ad_printf("System data dir: %s", newValue.c_str());
568 #else
569  // defined in build-info.hh (default /opt/openMSX/share)
570  newValue = DATADIR;
571 #endif
572  return newValue;
573 }
574 
575 #ifdef _WIN32
576 bool driveExists(char driveLetter)
577 {
578  char buf[] = { driveLetter, ':', 0 };
579  return GetFileAttributesA(buf) != INVALID_FILE_ATTRIBUTES;
580 }
581 #endif
582 
584 {
585  string result = path.str();
586 #ifdef _WIN32
587  if (((path.size() == 2) && (path[1] == ':')) ||
588  ((path.size() >= 3) && (path[1] == ':') && (path[2] != '/'))) {
589  // get current directory for this drive
590  unsigned char drive = tolower(path[0]);
591  if (('a' <= drive) && (drive <= 'z')) {
592  wchar_t bufW[MAXPATHLEN + 1];
593  if (driveExists(drive) &&
594  _wgetdcwd(drive - 'a' + 1, bufW, MAXPATHLEN)) {
595  result = getConventionalPath(utf16to8(bufW));
596  if (result.back() != '/') {
597  result += '/';
598  }
599  if (path.size() > 2) {
600  string_ref tmp = path.substr(2);
601  result.append(tmp.data(), tmp.size());
602  }
603  }
604  }
605  }
606 #endif
607  return result;
608 }
609 
610 bool getStat(const string& filename_, Stat& st)
611 {
612  string filename = expandTilde(filename_);
613  // workaround for VC++: strip trailing slashes (but keep it if it's the
614  // only character in the path)
615  auto pos = filename.find_last_not_of('/');
616  if (pos == string::npos) {
617  // string was either empty or a (sequence of) '/' character(s)
618  filename = filename.empty() ? "" : "/";
619  } else {
620  filename.resize(pos + 1);
621  }
622 #ifdef _WIN32
623  return _wstat(utf8to16(filename).c_str(), &st) == 0;
624 #else
625  return stat(filename.c_str(), &st) == 0;
626 #endif
627 }
628 
629 bool isRegularFile(const Stat& st)
630 {
631  return S_ISREG(st.st_mode);
632 }
633 bool isRegularFile(const string& filename)
634 {
635  Stat st;
636  return getStat(filename, st) && isRegularFile(st);
637 }
638 
639 bool isDirectory(const Stat& st)
640 {
641  return S_ISDIR(st.st_mode);
642 }
643 
644 bool isDirectory(const string& directory)
645 {
646  Stat st;
647  return getStat(directory, st) && isDirectory(st);
648 }
649 
650 bool exists(const string& filename)
651 {
652  Stat st; // dummy
653  return getStat(filename, st);
654 }
655 
656 time_t getModificationDate(const Stat& st)
657 {
658  return st.st_mtime;
659 }
660 
661 static int getNextNum(dirent* d, string_ref prefix, string_ref extension,
662  unsigned nofdigits)
663 {
664  auto extensionLen = extension.size();
665  auto prefixLen = prefix.size();
666  string_ref name(d->d_name);
667 
668  if ((name.size() != (prefixLen + nofdigits + extensionLen)) ||
669  (name.substr(0, prefixLen) != prefix) ||
670  (name.substr(prefixLen + nofdigits, extensionLen) != extension)) {
671  return 0;
672  }
673  string_ref num = name.substr(prefixLen, nofdigits);
675  unsigned long n = stoul(num, &idx, 10);
676  return (idx == num.size()) ? n : 0;
677 }
678 
680  string_ref directory, string_ref prefix, string_ref extension)
681 {
682  const unsigned nofdigits = 4;
683 
684  int max_num = 0;
685 
686  string dirName = getUserOpenMSXDir() + '/' + directory;
687  try {
688  mkdirp(dirName);
689  } catch (FileException&) {
690  // ignore
691  }
692 
693  ReadDir dir(dirName);
694  while (dirent* d = dir.getEntry()) {
695  max_num = std::max(max_num, getNextNum(d, prefix, extension, nofdigits));
696  }
697 
698  std::ostringstream os;
699  os << dirName << '/' << prefix;
700  os.width(nofdigits);
701  os.fill('0');
702  os << (max_num + 1) << extension;
703  return os.str();
704 }
705 
707  string_ref argument, string_ref directory,
708  string_ref prefix, string_ref extension)
709 {
710  if (argument.empty()) {
711  // directory is also created when needed
712  return getNextNumberedFileName(directory, prefix, extension);
713  }
714 
715  string filename = argument.str();
716  if (getBaseName(filename).empty()) {
717  // no dir given, use standard dir (and create it)
718  string dir = getUserOpenMSXDir() + '/' + directory;
719  mkdirp(dir);
720  filename = dir + '/' + filename;
721  } else {
722  filename = expandTilde(filename);
723  }
724 
725  if (!StringOp::endsWith(filename, extension) &&
726  !exists(filename)) {
727  // Expected extension not already given, append it. But only
728  // when the filename without extension doesn't already exist.
729  // Without this exception stuff like 'soundlog start /dev/null'
730  // reports an error " ... error opening file /dev/null.wav."
731  filename.append(extension.data(), extension.size());
732  }
733  return filename;
734 }
735 
736 string getTempDir()
737 {
738 #ifdef _WIN32
739  DWORD len = GetTempPathW(0, nullptr);
740  if (len) {
741  VLA(wchar_t, bufW, (len+1));
742  len = GetTempPathW(len, bufW);
743  if (len) {
744  // Strip last backslash
745  if (bufW[len-1] == L'\\') {
746  bufW[len-1] = L'\0';
747  }
748  return utf16to8(bufW);
749  }
750  }
751  throw FatalError(StringOp::Builder() <<
752  "GetTempPathW failed: " << GetLastError());
753 #elif PLATFORM_ANDROID
754  string result = getSystemDataDir() + "/tmp";
755  return result;
756 #else
757  const char* result = nullptr;
758  if (!result) result = getenv("TMPDIR");
759  if (!result) result = getenv("TMP");
760  if (!result) result = getenv("TEMP");
761  if (!result) {
762  result = "/tmp";
763  }
764  return result;
765 #endif
766 }
767 
768 FILE* openUniqueFile(const std::string& directory, std::string& filename)
769 {
770 #ifdef _WIN32
771  std::wstring directoryW = utf8to16(directory);
772  wchar_t filenameW[MAX_PATH];
773  if (!GetTempFileNameW(directoryW.c_str(), L"msx", 0, filenameW)) {
775  "GetTempFileNameW failed: " << GetLastError());
776  }
777  filename = utf16to8(filenameW);
778  FILE* fp = _wfopen(filenameW, L"wb");
779 #else
780  filename = directory + "/XXXXXX";
781  int fd = mkstemp(const_cast<char*>(filename.c_str()));
782  if (fd == -1) {
783  throw FileException("Coundn't get temp file name");
784  }
785  FILE* fp = fdopen(fd, "wb");
786 #endif
787  return fp;
788 }
789 
790 } // namespace FileOperations
791 
792 } // namespace openmsx