swift
fileutils.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2015 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
4 #include "misc/fileutils.h"
5 
6 #include <algorithm>
7 
8 #include <QCoreApplication>
9 #include <QDateTime>
10 #include <QFile>
11 #include <QFileInfo>
12 #include <QIODevice>
13 #include <QList>
14 #include <QLockFile>
15 #include <QRegularExpression>
16 #include <QStringBuilder>
17 #include <QTextStream>
18 #include <QtGlobal>
19 
20 #include "config/buildconfig.h"
21 #include "misc/setbuilder.h"
22 #include "misc/stringutils.h"
23 
24 using namespace swift::config;
25 
26 namespace swift::misc
27 {
28  const QString &CFileUtils::jsonAppendix()
29  {
30  static const QString j(".json");
31  return j;
32  }
33 
34  const QString &CFileUtils::jsonWildcardAppendix()
35  {
36  static const QString jw("*" + jsonAppendix());
37  return jw;
38  }
39 
40  bool CFileUtils::writeStringToFile(const QString &content, const QString &fileNameAndPath)
41  {
42  if (fileNameAndPath.isEmpty()) { return false; }
43  QFile file(fileNameAndPath);
44  if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return false; }
45  QTextStream stream(&file);
46  stream << content;
47  file.close();
48  return true;
49  }
50 
51  bool CFileUtils::writeByteArrayToFile(const QByteArray &data, const QString &fileNameAndPath)
52  {
53  if (fileNameAndPath.isEmpty()) { return false; }
54  QFile file(fileNameAndPath);
55  if (!file.open(QIODevice::WriteOnly)) { return false; }
56  const qint64 c = file.write(data);
57  file.close();
58  return c == data.size();
59  }
60 
61  bool CFileUtils::writeStringToLockedFile(const QString &content, const QString &fileNameAndPath)
62  {
63  QLockFile lock(fileNameAndPath + ".lock");
64  if (!lock.lock()) { return false; }
65  return writeStringToFile(content, fileNameAndPath);
66  }
67 
68  QString CFileUtils::readFileToString(const QString &fileNameAndPath)
69  {
70  QFile file(fileNameAndPath);
71  if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return {}; }
72  QTextStream stream(&file);
73  const QString content(stream.readAll());
74  file.close();
75  return content;
76  }
77 
78  QString CFileUtils::readLockedFileToString(const QString &fileNameAndPath)
79  {
80  QLockFile lock(fileNameAndPath + ".lock");
81  if (!lock.lock()) { return {}; }
82  return readFileToString(fileNameAndPath);
83  }
84 
85  QString CFileUtils::readFileToString(const QString &filePath, const QString &fileName)
86  {
87  return readFileToString(appendFilePaths(filePath, fileName));
88  }
89 
90  QString CFileUtils::readLockedFileToString(const QString &filePath, const QString &fileName)
91  {
92  return readLockedFileToString(appendFilePaths(filePath, fileName));
93  }
94 
95  QString CFileUtils::appendFilePaths(const QString &path1, const QString &path2)
96  {
97  if (path1.isEmpty()) { return QDir::cleanPath(path2); }
98  if (path2.isEmpty()) { return QDir::cleanPath(path1); }
99  if (path1.endsWith('/'))
100  {
101  // avoid double /
102  if (path2.startsWith('/')) { return QDir::cleanPath(path1 % path2.mid(1)); }
103  return QDir::cleanPath(path1 % path2);
104  }
105  return QDir::cleanPath(path1 % QChar('/') % path2);
106  }
107 
108  QString CFileUtils::appendFilePathsAndFixUnc(const QString &path1, const QString &path2)
109  {
110  static const bool win = CBuildConfig::isRunningOnWindowsNtPlatform();
111  return win ? CFileUtils::fixWindowsUncPath(appendFilePaths(path1, path2)) : appendFilePaths(path1, path2);
112  }
113 
114  QString CFileUtils::stripFileFromPath(const QString &path)
115  {
116  if (path.endsWith('/')) { return path; }
117  if (!path.contains('/')) { return path; }
118  return path.left(path.lastIndexOf('/'));
119  }
120 
121  QString CFileUtils::stripFirstSlashPart(const QString &path)
122  {
123  QString p = normalizeFilePathToQtStandard(path);
124  int i = p.indexOf('/');
125  if (i < 0) { return p; }
126  if ((i + 1) >= path.length()) { return {}; }
127  return path.mid(i + 1);
128  }
129 
130  QStringList CFileUtils::stripFirstSlashParts(const QStringList &paths)
131  {
132  QStringList stripped;
133  for (const QString &path : paths) { stripped.push_back(stripFileFromPath(path)); }
134  return stripped;
135  }
136 
137  QString CFileUtils::stripLeadingSlashOrDriveLetter(const QString &path)
138  {
139  thread_local const QRegularExpression re("^\\/+|^[a-zA-Z]:\\/*");
140  QString p(path);
141  return p.replace(re, "");
142  }
143 
144  QStringList CFileUtils::stripLeadingSlashOrDriveLetters(const QStringList &paths)
145  {
146  QStringList stripped;
147  for (const QString &path : paths) { stripped.push_back(stripLeadingSlashOrDriveLetter(path)); }
148  return stripped;
149  }
150 
151  QString CFileUtils::lastPathSegment(const QString &path)
152  {
153  if (path.isEmpty()) { return {}; }
154  if (path.endsWith('/')) { return CFileUtils::lastPathSegment(path.left(path.length() - 1)); }
155  if (!path.contains('/')) { return path; }
156  return path.mid(path.lastIndexOf('/') + 1);
157  }
158 
159  QString CFileUtils::appendFilePaths(const QString &path1, const QString &path2, const QString &path3)
160  {
161  return CFileUtils::appendFilePaths(CFileUtils::appendFilePaths(path1, path2), path3);
162  }
163 
164  QString CFileUtils::appendFilePathsAndFixUnc(const QString &path1, const QString &path2, const QString &path3)
165  {
166  static const bool win = CBuildConfig::isRunningOnWindowsNtPlatform();
167  return win ? CFileUtils::fixWindowsUncPath(
168  CFileUtils::appendFilePaths(CFileUtils::appendFilePaths(path1, path2), path3)) :
169  CFileUtils::appendFilePaths(CFileUtils::appendFilePaths(path1, path2), path3);
170  }
171 
172  QString CFileUtils::pathUp(const QString &path)
173  {
174  const int i = path.lastIndexOf('/');
175  if (i < 0) { return path; }
176  return path.left(i);
177  }
178 
179  QString CFileUtils::normalizeFilePathToQtStandard(const QString &filePath)
180  {
181  if (filePath.isEmpty()) { return {}; }
182  QString n = QDir::cleanPath(filePath);
183  n = n.replace('\\', '/').replace("//", "/"); // should be done alreay by cleanPath, paranoia
184  return n;
185  }
186 
187  QStringList CFileUtils::makeDirectoriesRelative(const QStringList &directories, const QString &rootDirectory,
189  {
190  // not using QDir::relativePath because I do not want "../xyz" paths
191  if (rootDirectory.isEmpty() || rootDirectory == "/") { return directories; }
192  const QString rd(rootDirectory.endsWith('/') ? rootDirectory.left(rootDirectory.length() - 1) : rootDirectory);
193  const int p = rd.length();
194  QStringList relativeDirectories;
195  for (const QString &dir : directories)
196  {
197  if (dir.startsWith(rd, cs) && dir.length() > p + 1) { relativeDirectories.append(dir.mid(p + 1)); }
198  else
199  {
200  relativeDirectories.append(dir); // absolute
201  }
202  }
203  return relativeDirectories;
204  }
205 
206  bool CFileUtils::sameDirectories(const QStringList &dirs1, const QStringList &dirs2, Qt::CaseSensitivity cs)
207  {
208  // clean up
209  QStringList dirs1Cleaned(dirs1);
210  QStringList dirs2Cleaned(dirs2);
211  dirs1Cleaned.removeAll("");
212  dirs1Cleaned.removeDuplicates();
213  dirs2Cleaned.removeAll("");
214  dirs2Cleaned.removeDuplicates();
215  if (dirs1Cleaned.size() != dirs2Cleaned.size()) { return false; }
216 
217  int d2 = 0;
218  dirs1Cleaned.sort(cs);
219  dirs2Cleaned.sort(cs);
220  return std::all_of(dirs1.cbegin(), dirs1.cend(),
221  [&](const QString &d1) { return stringCompare(d1, dirs2.at(d2), cs); });
222  }
223 
224  Qt::CaseSensitivity CFileUtils::osFileNameCaseSensitivity()
225  {
227  }
228 
229  bool CFileUtils::isFileNameCaseSensitive() { return CFileUtils::osFileNameCaseSensitivity() == Qt::CaseSensitive; }
230 
231  bool CFileUtils::matchesExcludeDirectory(const QString &directoryPath, const QString &excludePattern,
233  {
234  if (directoryPath.isEmpty() || excludePattern.isEmpty()) { return false; }
235  const QString normalizedExcludePattern(normalizeFilePathToQtStandard(excludePattern));
236  return directoryPath.contains(normalizedExcludePattern, cs);
237  }
238 
239  bool CFileUtils::isExcludedDirectory(const QDir &directory, const QStringList &excludeDirectories,
241  {
242  if (excludeDirectories.isEmpty()) { return false; }
243  const QString d = directory.absolutePath();
244  return isExcludedDirectory(d, excludeDirectories, cs);
245  }
246 
247  bool CFileUtils::isExcludedDirectory(const QFileInfo &fileInfo, const QStringList &excludeDirectories,
249  {
250  if (excludeDirectories.isEmpty()) { return false; }
251  return isExcludedDirectory(fileInfo.absoluteDir(), excludeDirectories, cs);
252  }
253 
254  bool CFileUtils::isExcludedDirectory(const QString &directoryPath, const QStringList &excludeDirectories,
256  {
257  if (excludeDirectories.isEmpty()) { return false; }
258  return std::any_of(excludeDirectories.cbegin(), excludeDirectories.cend(),
259  [&](const QString &ex) { return matchesExcludeDirectory(directoryPath, ex, cs); });
260  }
261 
262  QStringList CFileUtils::removeSubDirectories(const QStringList &directories, Qt::CaseSensitivity cs)
263  {
264  if (directories.size() < 2) { return directories; }
265  QStringList dirs(directories);
266  dirs.removeDuplicates();
267  dirs.sort(cs);
268  if (dirs.size() < 2) { return dirs; }
269 
270  QString last;
271  QStringList result;
272  for (const QString &path : std::as_const(dirs))
273  {
274  if (path.isEmpty()) { continue; }
275  if (last.isEmpty() || !path.startsWith(last, cs)) { result.append(path); }
276  last = path;
277  }
278  return result;
279  }
280 
281  QString CFileUtils::findFirstExisting(const QStringList &filesOrDirectory)
282  {
283  if (filesOrDirectory.isEmpty()) { return {}; }
284  for (const QString &f : filesOrDirectory)
285  {
286  if (f.isEmpty()) { continue; }
287  const QString fn(normalizeFilePathToQtStandard(f));
288  const QFileInfo fi(fn);
289  if (fi.exists()) { return fi.absoluteFilePath(); }
290  }
291  return {};
292  }
293 
294  QString CFileUtils::findFirstFile(const QDir &dir, bool recursive, const QStringList &nameFilters,
295  const QStringList &excludeDirectories,
296  std::function<bool(const QFileInfo &)> predicate)
297  {
298  if (isExcludedDirectory(dir, excludeDirectories)) { return {}; }
299  const QFileInfoList result = dir.entryInfoList(nameFilters, QDir::Files);
300  if (predicate)
301  {
302  auto it = std::find_if(result.cbegin(), result.cend(), predicate);
303  if (it != result.cend()) { return it->filePath(); }
304  }
305  else
306  {
307  if (!result.isEmpty()) { return result.first().filePath(); }
308  }
309  if (recursive)
310  {
311  for (const auto &subdir : dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
312  {
313  if (isExcludedDirectory(subdir, excludeDirectories)) { continue; }
314  const QString first =
315  findFirstFile(subdir.filePath(), true, nameFilters, excludeDirectories, predicate);
316  if (!first.isEmpty()) { return first; }
317  }
318  }
319  return {};
320  }
321 
322  bool CFileUtils::containsFile(const QDir &dir, bool recursive, const QStringList &nameFilters,
323  const QStringList &excludeDirectories,
324  std::function<bool(const QFileInfo &)> predicate)
325  {
326  return !findFirstFile(dir, recursive, nameFilters, excludeDirectories, predicate).isEmpty();
327  }
328 
329  QString CFileUtils::findFirstNewerThan(const QDateTime &time, const QDir &dir, bool recursive,
330  const QStringList &nameFilters, const QStringList &excludeDirectories)
331  {
332  return findFirstFile(dir, recursive, nameFilters, excludeDirectories,
333  [time](const QFileInfo &fi) { return fi.lastModified() > time; });
334  }
335 
336  bool CFileUtils::containsFileNewerThan(const QDateTime &time, const QDir &dir, bool recursive,
337  const QStringList &nameFilters, const QStringList &excludeDirectories)
338  {
339  return !findFirstNewerThan(time, dir, recursive, nameFilters, excludeDirectories).isEmpty();
340  }
341 
342  QFileInfoList CFileUtils::enumerateFiles(const QDir &dir, bool recursive, const QStringList &nameFilters,
343  const QStringList &excludeDirectories,
344  std::function<bool(const QFileInfo &)> predicate)
345  {
346  if (isExcludedDirectory(dir, excludeDirectories)) { return {}; }
347  QFileInfoList result = dir.entryInfoList(nameFilters, QDir::Files);
348  if (predicate)
349  {
350  result.erase(std::remove_if(result.begin(), result.end(), [=](const auto &f) { return !predicate(f); }),
351  result.end());
352  }
353  if (recursive)
354  {
355  for (const auto &subdir : dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
356  {
357  if (isExcludedDirectory(subdir, excludeDirectories)) { continue; }
358  result += enumerateFiles(subdir.filePath(), true, nameFilters, excludeDirectories, predicate);
359  }
360  }
361  return result;
362  }
363 
364  QFileInfo CFileUtils::findLastModified(const QDir &dir, bool recursive, const QStringList &nameFilters,
365  const QStringList &excludeDirectories)
366  {
367  if (isExcludedDirectory(dir, excludeDirectories)) { return {}; }
368  const QFileInfoList files = enumerateFiles(dir, recursive, nameFilters, excludeDirectories);
369  if (files.isEmpty()) { return {}; }
370 
371  auto it = std::max_element(files.cbegin(), files.cend(), [](const QFileInfo &a, const QFileInfo &b) {
372  return a.lastModified() < b.lastModified();
373  });
374  return *it;
375  }
376 
377  QFileInfo CFileUtils::findLastCreated(const QDir &dir, bool recursive, const QStringList &nameFilters,
378  const QStringList &excludeDirectories)
379  {
380  if (isExcludedDirectory(dir, excludeDirectories)) { return {}; }
381  const QFileInfoList files = enumerateFiles(dir, recursive, nameFilters, excludeDirectories);
382  if (files.isEmpty()) { return {}; }
383 
384  auto it = std::max_element(files.cbegin(), files.cend(), [](const QFileInfo &a, const QFileInfo &b) {
385  return a.birthTime() < b.birthTime();
386  });
387  return *it;
388  }
389 
390  const QStringList &CFileUtils::getSwiftExecutables()
391  {
392  static const QStringList executables(
394  return executables;
395  }
396 
397  QStringList CFileUtils::getBaseNamesOnly(const QStringList &fileNames)
398  {
399  QStringList baseNames;
400  for (const QString &fn : fileNames)
401  {
402  const QFileInfo fi(fn);
403  baseNames.push_back(fi.baseName());
404  }
405  return baseNames;
406  }
407 
408  QStringList CFileUtils::getFileNamesOnly(const QStringList &fileNames)
409  {
410  QStringList fns;
411  for (const QString &fn : fileNames)
412  {
413  const QFileInfo fi(fn);
414  fns.push_back(fi.fileName());
415  }
416  return fns;
417  }
418 
419  QString CFileUtils::lockFileError(const QLockFile &lockFile)
420  {
421  switch (lockFile.error())
422  {
423  case QLockFile::NoError: return QStringLiteral("No error");
424  case QLockFile::PermissionError: return QStringLiteral("Insufficient permission");
425  case QLockFile::UnknownError: return QStringLiteral("Unknown error");
427  {
428  QString hostname, appname;
429  qint64 pid = 0;
430  lockFile.getLockInfo(&pid, &hostname, &appname);
431  return QStringLiteral("Lock open in another process (%1 %2 on %3)")
432  .arg(hostname, QString::number(pid), appname);
433  }
434  default: return QStringLiteral("Bad error number");
435  }
436  }
437 
438  QString CFileUtils::fixWindowsUncPath(const QString &filePath)
439  {
440  static const bool win = CBuildConfig::isRunningOnWindowsNtPlatform();
441  if (!win) { return filePath; }
442  if (!filePath.startsWith('/')) { return filePath; }
443  if (filePath.startsWith("//")) { return filePath; }
444  return QStringLiteral("/%1").arg(filePath);
445  }
446 
447  QStringList CFileUtils::fixWindowsUncPaths(const QStringList &filePaths)
448  {
449  static const bool win = CBuildConfig::isRunningOnWindowsNtPlatform();
450  if (!win) { return filePaths; }
451 
452  QStringList fixedPaths;
453  for (const QString &path : filePaths) { fixedPaths << fixWindowsUncPath(path); }
454  return fixedPaths;
455  }
456 
457  bool CFileUtils::isWindowsUncPath(const QString &filePath)
458  {
459  if (filePath.startsWith("//") || filePath.startsWith("\\\\")) { return true; }
460  if (!CBuildConfig::isRunningOnWindowsNtPlatform()) { return false; } // "/tmp" is valid on Unix/Mac
461 
462  // Windows here
463  const QString fp = fixWindowsUncPath(filePath);
464  return (fp.startsWith("//") || fp.startsWith("\\\\"));
465  }
466 
467  QString CFileUtils::windowsUncMachine(const QString &filePath)
468  {
469  if (!CFileUtils::isWindowsUncPath(filePath)) { return {}; }
470  QString f = filePath;
471  f.replace("\\", "/");
472  f.replace("//", "");
473  if (f.startsWith("/")) { f = f.mid(1); }
474  const int i = f.indexOf('/');
475  if (i < 0) { return f; }
476  return f.left(i);
477  }
478 
479  QSet<QString> CFileUtils::windowsUncMachines(const QSet<QString> &paths)
480  {
481  if (paths.isEmpty()) { return {}; }
482 
483  const Qt::CaseSensitivity cs = osFileNameCaseSensitivity();
484  const bool isCs = isFileNameCaseSensitive();
485 
486  CSetBuilder<QString> machines;
487  QString lastMachine;
488 
489  for (const QString &p : paths)
490  {
491  if (!lastMachine.isEmpty() && p.contains(lastMachine, cs))
492  {
493  // shortcut
494  continue;
495  }
496  const QString m = isCs ? windowsUncMachine(p) : windowsUncMachine(p).toLower();
497  if (m.isEmpty()) { continue; }
498  lastMachine = m;
499  machines.insert(m);
500  }
501  return machines;
502  }
503 
504  QString CFileUtils::toWindowsLocalPath(const QString &path)
505  {
506  QString p = CFileUtils::fixWindowsUncPath(path);
507  return p.replace('/', '\\');
508  }
509 
510  QString CFileUtils::humanReadableFileSize(qint64 size)
511  {
512  // from https://stackoverflow.com/a/30958189/356726
513  // fell free to replace it by something better
514  static const QStringList units({ "KB", "MB", "GB", "TB" });
515  if (size <= 1024) { return QString::number(size); }
516 
517  QStringListIterator i(units);
518  double currentSize = size;
519  QString unit;
520  while (currentSize >= 1024.0 && i.hasNext())
521  {
522  unit = i.next();
523  currentSize /= 1024.0;
524  }
525  return QStringLiteral("%1 %2").arg(currentSize, 0, 'f', 2).arg(unit);
526  }
527 
528  const QStringList &CFileUtils::executableSuffixes()
529  {
530  // incomplete list of file name appendixes
531  // dmg is not a executable. It is a MacOS container. If you open it, a new virtual drive will be mapped which
532  // includes a executable.
533  static const QStringList appendixes({ ".exe", ".dmg", ".run" });
534  return appendixes;
535  }
536 
537  bool CFileUtils::isExecutableFile(const QString &fileName)
538  {
539  for (const QString &app : CFileUtils::executableSuffixes())
540  {
541  if (fileName.endsWith(app, Qt::CaseInsensitive)) { return true; }
542  }
543  return CFileUtils::isSwiftInstaller(fileName);
544  }
545 
546  bool CFileUtils::isSwiftInstaller(const QString &fileName)
547  {
548  if (fileName.isEmpty()) { return false; }
549  return fileName.contains("swift", Qt::CaseInsensitive) && fileName.contains("installer");
550  }
551 } // namespace swift::misc
static constexpr bool isRunningOnWindowsNtPlatform()
Running on Windows NT platform?
Build a QSet more efficiently when calling insert() in a for loop.
Definition: setbuilder.h:25
void insert(const T &value)
Add an element to the set. Runs in amortized constant time.
Definition: setbuilder.h:29
Free functions in swift::misc.
qsizetype size() const const
QString applicationFilePath()
QString absolutePath() const const
QString cleanPath(const QString &path)
QFileInfoList entryInfoList(QDir::Filters filters, QDir::SortFlags sort) const const
bool open(FILE *fh, QIODeviceBase::OpenMode mode, QFileDevice::FileHandleFlags handleFlags)
virtual void close() override
QDir absoluteDir() const const
QString absoluteFilePath() const const
QString baseName() const const
bool exists(const QString &path)
QString fileName() const const
QDateTime lastModified() const const
qint64 write(const QByteArray &data)
void append(QList< T > &&value)
QList< T >::const_iterator cbegin() const const
QList< T >::const_iterator cend() const const
bool isEmpty() const const
void push_back(QList< T >::parameter_type value)
qsizetype removeAll(const AT &t)
qsizetype size() const const
QLockFile::LockError error() const const
bool getLockInfo(qint64 *pid, QString *hostname, QString *appname) const const
bool lock()
bool isEmpty() const const
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) &&
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) &&
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype removeDuplicates()
void sort(Qt::CaseSensitivity cs)
CaseSensitivity
QString readAll()