datapaths.cpp
1 //------------------------------------------------------------------------------
2 // datapaths.cpp
3 //------------------------------------------------------------------------------
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 // 02110-1301 USA
19 //
20 //------------------------------------------------------------------------------
21 // Copyright (C) 2010 "Zalewa" <zalewapl@gmail.com>
22 //------------------------------------------------------------------------------
23 #include "datapaths.h"
24 
25 #include "application.h"
26 #include "doomseekerfilepaths.h"
27 #include "fileutils.h"
28 #include "log.h"
29 #include "plugins/engineplugin.h"
30 #include "strings.hpp"
31 
32 #include <cassert>
33 #include <cerrno>
34 #include <cstdlib>
35 #include <QCoreApplication>
36 #include <QDesktopServices>
37 #include <QFileInfo>
38 #include <QProcessEnvironment>
39 #include <QSet>
40 
41 #include <QStandardPaths>
42 
43 // Sanity check for INSTALL_PREFIX and INSTALL_LIBDIR
44 #if !defined(INSTALL_PREFIX) || !defined(INSTALL_LIBDIR)
45 #error Build system should provide definition for INSTALL_PREFIX and INSTALL_LIBDIR
46 #endif
47 
48 // On NTFS file systems, ownership and permissions checking is disabled by
49 // default for performance reasons. The following int toggles it by
50 // incrementation and decrementation of its value.
51 // See: http://doc.qt.io/qt-5/qfileinfo.html#ntfs-permissions
52 #ifdef Q_OS_WIN32
53 extern Q_CORE_EXPORT int qt_ntfs_permission_lookup;
54 #else
55 // We'll need to declare an int with the same name to compile successfully in other platforms.
56 int qt_ntfs_permission_lookup;
57 #endif
58 
59 static QList<DataPaths::DirErrno> uniqueErrnosByDir(const QList<DataPaths::DirErrno> &errnos)
60 {
61  QSet<QString> uniqueDirs;
62  QList<DataPaths::DirErrno> uniqueErrnos;
63  for (const DataPaths::DirErrno &dirErrno : errnos)
64  {
65  if (!uniqueDirs.contains(dirErrno.directory.path()))
66  {
67  uniqueDirs.insert(dirErrno.directory.path());
68  uniqueErrnos << dirErrno;
69  }
70  }
71  return uniqueErrnos;
72 }
73 
74 static QStringList uniquePaths(const QStringList &paths)
75 {
76  QList<QFileInfo> uniqueMarkers;
77  QStringList result;
78  for (const QString &path : paths)
79  {
80  QFileInfo fileInfo(path);
81  if (!uniqueMarkers.contains(fileInfo))
82  {
83  uniqueMarkers << fileInfo;
84  result << path;
85  }
86  }
87  return result;
88 }
89 
90 DClass<DataPaths>
91 {
92 public:
93  static const QString PLUGINS_DIR_NAME;
94 
95  QDir cacheDirectory;
96  QDir configDirectory;
97  QDir dataDirectory;
98 
99  QString workingDirectory;
100 
101  bool bIsPortableModeOn;
102 };
103 
104 DPointered(DataPaths)
105 
106 DataPaths *DataPaths::staticDefaultInstance = nullptr;
107 
108 static const QString LEGACY_APPDATA_DIR_NAME = ".doomseeker";
109 static const QString DEMOS_DIR_NAME = "demos";
110 
111 const QString DataPaths::CHATLOGS_DIR_NAME = "chatlogs";
112 const QString PrivData<DataPaths>::PLUGINS_DIR_NAME = "plugins";
113 const QString DataPaths::TRANSLATIONS_DIR_NAME = "translations";
114 const QString DataPaths::UPDATE_PACKAGES_DIR_NAME = "updates";
115 const QString DataPaths::UPDATE_PACKAGE_FILENAME_PREFIX = "doomseeker-update-pkg-";
116 
117 DataPaths::DataPaths(bool bPortableModeOn)
118 {
119  d->bIsPortableModeOn = bPortableModeOn;
120 
121  // Logically this would be "./" but our only use of this class as of
122  // Doomseeker 1.1 would use setWorkingDirectory to applicationDirPath()
123  d->workingDirectory = QCoreApplication::applicationDirPath();
124 
125  if (bPortableModeOn)
126  {
127  d->cacheDirectory.setPath(systemAppDataDirectory(".cache"));
128  d->configDirectory.setPath(systemAppDataDirectory(LEGACY_APPDATA_DIR_NAME));
129  d->dataDirectory.setPath(systemAppDataDirectory(".static"));
130  }
131  else
132  {
133  d->cacheDirectory.setPath(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
134  #if QT_VERSION >= 0x050500
135  // QStandardPaths::AppConfigLocation was added in Qt 5.5.
136  d->configDirectory.setPath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
137  #else
138  // In older 5.x versions we need to construct the config path ourselves.
139  d->configDirectory.setPath(Strings::combinePaths(
140  QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation),
142  #endif
143  d->dataDirectory.setPath(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
144  }
145 
146  gLog << QString("Cache directory: %1").arg(d->cacheDirectory.absolutePath());
147  gLog << QString("Config directory: %1").arg(d->configDirectory.absolutePath());
148  gLog << QString("Data directory: %1").arg(d->dataDirectory.absolutePath());
149 }
150 
151 DataPaths::~DataPaths()
152 {
153 }
154 
156 {
157  return d->cacheDirectory.absolutePath();
158 }
159 
160 QStringList DataPaths::canWrite() const
161 {
162  QStringList failedList;
163 
164  QString dataDirectory = programsDataDirectoryPath();
165  if (!validateDir(dataDirectory))
166  failedList.append(dataDirectory);
167 
168  return failedList;
169 }
170 
171 QList<DataPaths::DirErrno> DataPaths::createDirectories()
172 {
173  QList<DirErrno> failedDirs;
174  const QDir appDataDir(systemAppDataDirectory());
175 
176  // No need to bother with migrating plugin master caches
177  DirErrno cacheDirError = tryCreateDirectory(d->cacheDirectory, ".");
178  if (cacheDirError.isError())
179  failedDirs << cacheDirError;
180 
181  // The existential question here is needed for migration purposes,
182  // but on Windows the >=1.2 configDirectory can already exist because
183  // Doomseeker <1.2 already stored IRC chat logs there.
184  // It is necessary to ask about the .ini file.
185  if (!d->configDirectory.exists(DoomseekerFilePaths::INI_FILENAME))
186  {
187  DirErrno configDirError = tryCreateDirectory(d->configDirectory, ".");
188  if (configDirError.isError())
189  failedDirs << configDirError;
190 
191  #if !defined(Q_OS_DARWIN)
192  else if (appDataDir.exists(".doomseeker"))
193  {
194  // Migrate config from old versions of Doomseeker (pre 1.2)
195  const QDir oldConfigDir(appDataDir.absolutePath() + QDir::separator() + ".doomseeker");
196  gLog << QString("Migrating configuration data from '%1'\n\tto '%2'.")
197  .arg(oldConfigDir.absolutePath())
198  .arg(d->configDirectory.absolutePath());
199 
200  for (QFileInfo fileinfo : oldConfigDir.entryInfoList(QStringList("*.ini"), QDir::Files))
201  {
202  QFile(fileinfo.absoluteFilePath()).copy(d->configDirectory.absoluteFilePath(fileinfo.fileName()));
203  }
204  }
205  #endif
206  }
207 
208  // In >=1.2 and on Windows platform, dataDirectory can be the same
209  // as configDirectory. To do the migration properly, we need to
210  // ask about existence of a subdirectory inside the data directory.
211  if (!d->dataDirectory.exists(DEMOS_DIR_NAME))
212  {
213  #ifdef Q_OS_DARWIN
214  const QString legacyPrefDirectory = "Library/Preferences/Doomseeker";
215  #else
216  const QString legacyPrefDirectory = ".doomseeker";
217  #endif
218  DirErrno dataDirError = tryCreateDirectory(d->dataDirectory, ".");
219  if (dataDirError.isError())
220  failedDirs << dataDirError;
221  else if (appDataDir.exists(legacyPrefDirectory))
222  {
223  // Migrate data from old versions of Doomseeker (specifically demos) (pre 1.2)
224  const QDir oldConfigDir(appDataDir.absolutePath() + QDir::separator() + legacyPrefDirectory);
225  gLog << QString("Migrating user data from '%1'\n\tto '%2'.")
226  .arg(oldConfigDir.absolutePath())
227  .arg(d->dataDirectory.absolutePath());
228 
229  for (QFileInfo fileinfo : oldConfigDir.entryInfoList(QDir::Dirs))
230  {
231  const QString origPath = fileinfo.absoluteFilePath();
232  QFile file(origPath);
233  if (file.rename(d->dataDirectory.absoluteFilePath(fileinfo.fileName())))
234  {
235  // On Windows this will create an useless .lnk shortcut
236  // without the .lnk extension, so don't bother.
237  #if !defined(Q_OS_WIN32)
238  file.link(origPath);
239  #endif
240  }
241  }
242  }
243  }
244 
245  DirErrno demosDirError = tryCreateDirectory(d->dataDirectory, DEMOS_DIR_NAME);
246  if (demosDirError.isError())
247  failedDirs << demosDirError;
248 
249  return uniqueErrnosByDir(failedDirs);
250 }
251 
253 {
254  return staticDefaultInstance;
255 }
256 
257 
258 QStringList DataPaths::defaultWadPaths() const
259 {
260  QStringList filePaths;
261  filePaths << programsDataDirectoryPath();
262 
263  // The directory which contains the Doomseeker executable may be a good
264  // default choice, but on unix systems the bin directory is not worth
265  // searching. Similarly for Mac application bundle.
266  const QString progBinDirName = QDir(workingDirectory()).dirName();
267  if (progBinDirName != "bin" && progBinDirName != "MacOS")
268  filePaths << workingDirectory();
269 
270  return filePaths;
271 }
272 
273 QString DataPaths::demosDirectoryPath() const
274 {
275  return d->dataDirectory.absoluteFilePath(DEMOS_DIR_NAME);
276 }
277 
278 QString DataPaths::documentsLocationPath(const QString &subpath) const
279 {
280  QString rootPath;
281  if (!isPortableModeOn())
282  {
283  rootPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first();
284  rootPath = Strings::combinePaths(rootPath, QCoreApplication::applicationName());
285  }
286  else
287  rootPath = systemAppDataDirectory("storage");
288  return Strings::combinePaths(rootPath, subpath);
289 }
290 
291 QString DataPaths::env(const QString &key)
292 {
293  return QProcessEnvironment::systemEnvironment().value(key);
294 }
295 
296 void DataPaths::initDefault(bool bPortableModeOn)
297 {
298  assert(staticDefaultInstance == nullptr && "DataPaths can have only one default.");
299  if (staticDefaultInstance == nullptr)
300  staticDefaultInstance = new DataPaths(bPortableModeOn);
301 }
302 
303 bool DataPaths::isPortableModeOn() const
304 {
305  return d->bIsPortableModeOn;
306 }
307 
308 QString DataPaths::localDataLocationPath(const QString &subpath) const
309 {
310  return Strings::combinePaths(d->dataDirectory.absolutePath(), subpath);
311 }
312 
314 {
315  return localDataLocationPath(QString("%1/%2").arg(
317 }
318 
320 {
321  return documentsLocationPath(QString("%1/%2").arg(
323 }
324 
326 {
327  QStringList paths;
328  paths.append(workingDirectory());
329  paths.append("./");
330 
331  #if !defined(Q_OS_DARWIN) && !defined(Q_OS_WIN32)
332  // On systems where we install to a fixed location, if we see that we are
333  // running an installed binary, then we should only load plugins from the
334  // expected location. Otherwise use it only as a last resort.
335  const QString installDir = INSTALL_PREFIX "/" INSTALL_LIBDIR "/doomseeker/";
336  if (workingDirectory() == INSTALL_PREFIX "/bin")
337  paths = QStringList(installDir);
338  else
339  paths.append(installDir);
340  #endif
341 
342  paths = uniquePaths(paths);
343  return Strings::combineManyPaths(paths, "engines/");
344 }
345 
346 QString DataPaths::programFilesDirectory(MachineType machineType)
347 {
348  #ifdef Q_OS_WIN32
349  QString envVarName = "";
350 
351  switch (machineType)
352  {
353  case x86:
354  envVarName = "ProgramFiles(x86)";
355  break;
356 
357  case x64:
358  envVarName = "ProgramW6432";
359  break;
360 
361  case Preferred:
362  envVarName = "ProgramFiles";
363  break;
364 
365  default:
366  return QString();
367  }
368 
369  QString path = env(envVarName);
370  if (path.isEmpty() && machineType != Preferred)
371  {
372  // Empty outcome may happen on 32-bit systems where variables
373  // like "ProgramFiles(x86)" may not exist.
374  //
375  // If "ProgramFiles" variable is empty then something is seriously
376  // wrong with the system.
377  path = programFilesDirectory(Preferred);
378  }
379 
380  return path;
381 
382  #else
383  Q_UNUSED(machineType);
384  return QString();
385  #endif
386 }
387 
389 {
390  return d->configDirectory.absolutePath();
391 }
392 
393 void DataPaths::setWorkingDirectory(const QString &workingDirectory)
394 {
395  d->workingDirectory = workingDirectory;
396 }
397 
398 QStringList DataPaths::staticDataSearchDirs(const QString &subdir)
399 {
400  QStringList paths;
401  paths.append(QDir::currentPath()); // current working dir
402  paths.append(QCoreApplication::applicationDirPath()); // where exe is located
403  #ifndef Q_OS_WIN32
404  paths.append(INSTALL_PREFIX "/share/doomseeker"); // standard arch independent linux path
405  #endif
406  paths = uniquePaths(paths);
407  QString subdirFiltered = subdir.trimmed();
408  if (!subdirFiltered.isEmpty())
409  {
410  for (int i = 0; i < paths.size(); ++i)
411  paths[i] = Strings::combinePaths(paths[i], subdirFiltered);
412  }
413  return paths;
414 }
415 
416 QString DataPaths::systemAppDataDirectory(QString append) const
417 {
418  Strings::triml(append, "/\\");
419 
420  if (isPortableModeOn())
421  {
422  QString path = d->workingDirectory + "/" + append;
423  return QDir(path).absolutePath();
424  }
425 
426  // For non-portable model this continues here:
427  QString dir;
428 
429  #ifdef Q_OS_WIN32
430  // Let's open new block to prevent variable "bleeding".
431  {
432  QString envVar = env("APPDATA");
433  if (validateDir(envVar))
434  dir = envVar;
435  }
436  #endif
437 
438  if (dir.isEmpty())
439  {
440  dir = QDir::homePath();
441  if (!validateDir(dir))
442  return QString();
443  }
444 
445  Strings::trimr(dir, "/\\");
446 
447  dir += QDir::separator() + append;
448 
449  return QDir(dir).absolutePath();
450 }
451 
452 DataPaths::DirErrno DataPaths::tryCreateDirectory(const QDir &rootDir, const QString &dirToCreate) const
453 {
454  QString fullDirPath = Strings::combinePaths(rootDir.path(), dirToCreate);
455 
456  // We need to reset errno to prevent false positives
457  errno = 0;
458  if (!rootDir.mkpath(dirToCreate))
459  {
460  int errnoval = errno;
461  if (errnoval != 0)
462  return DirErrno(fullDirPath, errnoval, strerror(errnoval));
463  else
464  {
465  // Try to decipher if we have permissions.
466  // First we must find the bottom-most directory that does actually exist.
467  QString pathToBottomMostExisting = FileUtils::cdUpUntilExists(
468  Strings::combinePaths(rootDir.path(), dirToCreate));
469  // If we've found a bottom-most directory that exists, we can check its permissions.
470  if (!pathToBottomMostExisting.isEmpty())
471  {
472  QFileInfo parentDir(pathToBottomMostExisting);
473  if (parentDir.exists() && !parentDir.isDir())
474  {
475  return DirErrno(fullDirPath, DirErrno::CUSTOM_ERROR,
476  QObject::tr("parent node is not a directory: %1")
477  .arg(parentDir.filePath()));
478  }
479 
480  // BLOCK - keep this to absolute mininum
481  ++qt_ntfs_permission_lookup;
482  bool permissions = parentDir.isReadable()
483  && parentDir.isWritable()
484  && parentDir.isExecutable();
485  --qt_ntfs_permission_lookup;
486  // END OF BLOCK
487  if (!permissions)
488  {
489  return DirErrno(fullDirPath, DirErrno::CUSTOM_ERROR,
490  QObject::tr("lack of necessary permissions to the parent directory: %1")
491  .arg(parentDir.filePath()));
492  }
493  }
494  // Just give up trying to deduce a correct error.
495  return DirErrno(fullDirPath, DirErrno::CUSTOM_ERROR, QObject::tr("cannot create directory"));
496  }
497  }
498  return DirErrno();
499 }
500 
502 {
504 }
505 
506 bool DataPaths::validateDir(const QString &path)
507 {
508  QFileInfo fileInfo(path);
509 
510  bool bCondition1 = !path.isEmpty();
511  bool bCondition2 = fileInfo.exists();
512  bool bCondition3 = fileInfo.isDir();
513 
514  return bCondition1 && bCondition2 && bCondition3;
515 }
516 
517 const QString &DataPaths::workingDirectory() const
518 {
519  return d->workingDirectory;
520 }