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.count();
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,
188  Qt::CaseSensitivity cs)
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  for (const QString &d1 : dirs1)
221  {
222  if (!stringCompare(d1, dirs2.at(d2), cs)) { return false; }
223  }
224  return true;
225  }
226 
227  Qt::CaseSensitivity CFileUtils::osFileNameCaseSensitivity()
228  {
229  return CBuildConfig::isRunningOnWindowsNtPlatform() ? Qt::CaseInsensitive : Qt::CaseSensitive;
230  }
231 
232  bool CFileUtils::isFileNameCaseSensitive() { return CFileUtils::osFileNameCaseSensitivity() == Qt::CaseSensitive; }
233 
234  bool CFileUtils::matchesExcludeDirectory(const QString &directoryPath, const QString &excludePattern,
235  Qt::CaseSensitivity cs)
236  {
237  if (directoryPath.isEmpty() || excludePattern.isEmpty()) { return false; }
238  const QString normalizedExcludePattern(normalizeFilePathToQtStandard(excludePattern));
239  return directoryPath.contains(normalizedExcludePattern, cs);
240  }
241 
242  bool CFileUtils::isExcludedDirectory(const QDir &directory, const QStringList &excludeDirectories,
243  Qt::CaseSensitivity cs)
244  {
245  if (excludeDirectories.isEmpty()) { return false; }
246  const QString d = directory.absolutePath();
247  return isExcludedDirectory(d, excludeDirectories, cs);
248  }
249 
250  bool CFileUtils::isExcludedDirectory(const QFileInfo &fileInfo, const QStringList &excludeDirectories,
251  Qt::CaseSensitivity cs)
252  {
253  if (excludeDirectories.isEmpty()) { return false; }
254  return isExcludedDirectory(fileInfo.absoluteDir(), excludeDirectories, cs);
255  }
256 
257  bool CFileUtils::isExcludedDirectory(const QString &directoryPath, const QStringList &excludeDirectories,
258  Qt::CaseSensitivity cs)
259  {
260  if (excludeDirectories.isEmpty()) { return false; }
261  for (const QString &ex : excludeDirectories)
262  {
263  if (matchesExcludeDirectory(directoryPath, ex, cs)) { return true; }
264  }
265  return false;
266  }
267 
268  QStringList CFileUtils::removeSubDirectories(const QStringList &directories, Qt::CaseSensitivity cs)
269  {
270  if (directories.size() < 2) { return directories; }
271  QStringList dirs(directories);
272  dirs.removeDuplicates();
273  dirs.sort(cs);
274  if (dirs.size() < 2) { return dirs; }
275 
276  QString last;
277  QStringList result;
278  for (const QString &path : std::as_const(dirs))
279  {
280  if (path.isEmpty()) { continue; }
281  if (last.isEmpty() || !path.startsWith(last, cs)) { result.append(path); }
282  last = path;
283  }
284  return result;
285  }
286 
287  QString CFileUtils::findFirstExisting(const QStringList &filesOrDirectory)
288  {
289  if (filesOrDirectory.isEmpty()) { return {}; }
290  for (const QString &f : filesOrDirectory)
291  {
292  if (f.isEmpty()) { continue; }
293  const QString fn(normalizeFilePathToQtStandard(f));
294  const QFileInfo fi(fn);
295  if (fi.exists()) { return fi.absoluteFilePath(); }
296  }
297  return {};
298  }
299 
300  QString CFileUtils::findFirstFile(const QDir &dir, bool recursive, const QStringList &nameFilters,
301  const QStringList &excludeDirectories,
302  std::function<bool(const QFileInfo &)> predicate)
303  {
304  if (isExcludedDirectory(dir, excludeDirectories)) { return QString(); }
305  const QFileInfoList result = dir.entryInfoList(nameFilters, QDir::Files);
306  if (predicate)
307  {
308  auto it = std::find_if(result.cbegin(), result.cend(), predicate);
309  if (it != result.cend()) { return it->filePath(); }
310  }
311  else
312  {
313  if (!result.isEmpty()) { return result.first().filePath(); }
314  }
315  if (recursive)
316  {
317  for (const auto &subdir : dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
318  {
319  if (isExcludedDirectory(subdir, excludeDirectories)) { continue; }
320  const QString first =
321  findFirstFile(subdir.filePath(), true, nameFilters, excludeDirectories, predicate);
322  if (!first.isEmpty()) { return first; }
323  }
324  }
325  return {};
326  }
327 
328  bool CFileUtils::containsFile(const QDir &dir, bool recursive, const QStringList &nameFilters,
329  const QStringList &excludeDirectories,
330  std::function<bool(const QFileInfo &)> predicate)
331  {
332  return !findFirstFile(dir, recursive, nameFilters, excludeDirectories, predicate).isEmpty();
333  }
334 
335  QString CFileUtils::findFirstNewerThan(const QDateTime &time, const QDir &dir, bool recursive,
336  const QStringList &nameFilters, const QStringList &excludeDirectories)
337  {
338  return findFirstFile(dir, recursive, nameFilters, excludeDirectories,
339  [time](const QFileInfo &fi) { return fi.lastModified() > time; });
340  }
341 
342  bool CFileUtils::containsFileNewerThan(const QDateTime &time, const QDir &dir, bool recursive,
343  const QStringList &nameFilters, const QStringList &excludeDirectories)
344  {
345  return !findFirstNewerThan(time, dir, recursive, nameFilters, excludeDirectories).isEmpty();
346  }
347 
348  QFileInfoList CFileUtils::enumerateFiles(const QDir &dir, bool recursive, const QStringList &nameFilters,
349  const QStringList &excludeDirectories,
350  std::function<bool(const QFileInfo &)> predicate)
351  {
352  if (isExcludedDirectory(dir, excludeDirectories)) { return QFileInfoList(); }
353  QFileInfoList result = dir.entryInfoList(nameFilters, QDir::Files);
354  if (predicate)
355  {
356  result.erase(std::remove_if(result.begin(), result.end(), [=](const auto &f) { return !predicate(f); }),
357  result.end());
358  }
359  if (recursive)
360  {
361  for (const auto &subdir : dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
362  {
363  if (isExcludedDirectory(subdir, excludeDirectories)) { continue; }
364  result += enumerateFiles(subdir.filePath(), true, nameFilters, excludeDirectories, predicate);
365  }
366  }
367  return result;
368  }
369 
370  QFileInfo CFileUtils::findLastModified(const QDir &dir, bool recursive, const QStringList &nameFilters,
371  const QStringList &excludeDirectories)
372  {
373  if (isExcludedDirectory(dir, excludeDirectories)) { return {}; }
374  const QFileInfoList files = enumerateFiles(dir, recursive, nameFilters, excludeDirectories);
375  if (files.isEmpty()) { return {}; }
376 
377  auto it = std::max_element(files.cbegin(), files.cend(), [](const QFileInfo &a, const QFileInfo &b) {
378  return a.lastModified() < b.lastModified();
379  });
380  return *it;
381  }
382 
383  QFileInfo CFileUtils::findLastCreated(const QDir &dir, bool recursive, const QStringList &nameFilters,
384  const QStringList &excludeDirectories)
385  {
386  if (isExcludedDirectory(dir, excludeDirectories)) { return {}; }
387  const QFileInfoList files = enumerateFiles(dir, recursive, nameFilters, excludeDirectories);
388  if (files.isEmpty()) { return {}; }
389 
390  auto it = std::max_element(files.cbegin(), files.cend(), [](const QFileInfo &a, const QFileInfo &b) {
391  return a.birthTime() < b.birthTime();
392  });
393  return *it;
394  }
395 
396  const QStringList &CFileUtils::getSwiftExecutables()
397  {
398  static const QStringList executables(
399  QFileInfo(QCoreApplication::applicationFilePath()).dir().entryList(QDir::Executable | QDir::Files));
400  return executables;
401  }
402 
403  QStringList CFileUtils::getBaseNamesOnly(const QStringList &fileNames)
404  {
405  QStringList baseNames;
406  for (const QString &fn : fileNames)
407  {
408  const QFileInfo fi(fn);
409  baseNames.push_back(fi.baseName());
410  }
411  return baseNames;
412  }
413 
414  QStringList CFileUtils::getFileNamesOnly(const QStringList &fileNames)
415  {
416  QStringList fns;
417  for (const QString &fn : fileNames)
418  {
419  const QFileInfo fi(fn);
420  fns.push_back(fi.fileName());
421  }
422  return fns;
423  }
424 
425  QString CFileUtils::lockFileError(const QLockFile &lockFile)
426  {
427  switch (lockFile.error())
428  {
429  case QLockFile::NoError: return QStringLiteral("No error");
430  case QLockFile::PermissionError: return QStringLiteral("Insufficient permission");
431  case QLockFile::UnknownError: return QStringLiteral("Unknown error");
432  case QLockFile::LockFailedError:
433  {
434  QString hostname, appname;
435  qint64 pid = 0;
436  lockFile.getLockInfo(&pid, &hostname, &appname);
437  return QStringLiteral("Lock open in another process (%1 %2 on %3)")
438  .arg(hostname, QString::number(pid), appname);
439  }
440  default: return QStringLiteral("Bad error number");
441  }
442  }
443 
444  QString CFileUtils::fixWindowsUncPath(const QString &filePath)
445  {
446  static const bool win = CBuildConfig::isRunningOnWindowsNtPlatform();
447  if (!win) { return filePath; }
448  if (!filePath.startsWith('/')) { return filePath; }
449  if (filePath.startsWith("//")) { return filePath; }
450  return QStringLiteral("/%1").arg(filePath);
451  }
452 
453  QStringList CFileUtils::fixWindowsUncPaths(const QStringList &filePaths)
454  {
455  static const bool win = CBuildConfig::isRunningOnWindowsNtPlatform();
456  if (!win) { return filePaths; }
457 
458  QStringList fixedPaths;
459  for (const QString &path : filePaths) { fixedPaths << fixWindowsUncPath(path); }
460  return fixedPaths;
461  }
462 
463  bool CFileUtils::isWindowsUncPath(const QString &filePath)
464  {
465  if (filePath.startsWith("//") || filePath.startsWith("\\\\")) { return true; }
466  if (!CBuildConfig::isRunningOnWindowsNtPlatform()) { return false; } // "/tmp" is valid on Unix/Mac
467 
468  // Windows here
469  const QString fp = fixWindowsUncPath(filePath);
470  return (fp.startsWith("//") || fp.startsWith("\\\\"));
471  }
472 
473  QString CFileUtils::windowsUncMachine(const QString &filePath)
474  {
475  if (!CFileUtils::isWindowsUncPath(filePath)) { return {}; }
476  QString f = filePath;
477  f.replace("\\", "/");
478  f.replace("//", "");
479  if (f.startsWith("/")) { f = f.mid(1); }
480  const int i = f.indexOf('/');
481  if (i < 0) { return f; }
482  return f.left(i);
483  }
484 
485  QSet<QString> CFileUtils::windowsUncMachines(const QSet<QString> &paths)
486  {
487  if (paths.isEmpty()) { return {}; }
488 
489  const Qt::CaseSensitivity cs = osFileNameCaseSensitivity();
490  const bool isCs = isFileNameCaseSensitive();
491 
492  CSetBuilder<QString> machines;
493  QString lastMachine;
494 
495  for (const QString &p : paths)
496  {
497  if (!lastMachine.isEmpty() && p.contains(lastMachine, cs))
498  {
499  // shortcut
500  continue;
501  }
502  const QString m = isCs ? windowsUncMachine(p) : windowsUncMachine(p).toLower();
503  if (m.isEmpty()) { continue; }
504  lastMachine = m;
505  machines.insert(m);
506  }
507  return machines;
508  }
509 
510  QString CFileUtils::toWindowsLocalPath(const QString &path)
511  {
512  QString p = CFileUtils::fixWindowsUncPath(path);
513  return p.replace('/', '\\');
514  }
515 
516  QString CFileUtils::humanReadableFileSize(qint64 size)
517  {
518  // from https://stackoverflow.com/a/30958189/356726
519  // fell free to replace it by something better
520  static const QStringList units({ "KB", "MB", "GB", "TB" });
521  if (size <= 1024) { return QString::number(size); }
522 
523  QStringListIterator i(units);
524  double currentSize = size;
525  QString unit;
526  while (currentSize >= 1024.0 && i.hasNext())
527  {
528  unit = i.next();
529  currentSize /= 1024.0;
530  }
531  return QStringLiteral("%1 %2").arg(currentSize, 0, 'f', 2).arg(unit);
532  }
533 
534  const QStringList &CFileUtils::executableSuffixes()
535  {
536  // incomplete list of file name appendixes
537  // dmg is not a executable. It is a MacOS container. If you open it, a new virtual drive will be mapped which
538  // includes a executable.
539  static const QStringList appendixes({ ".exe", ".dmg", ".run" });
540  return appendixes;
541  }
542 
543  bool CFileUtils::isExecutableFile(const QString &fileName)
544  {
545  for (const QString &app : CFileUtils::executableSuffixes())
546  {
547  if (fileName.endsWith(app, Qt::CaseInsensitive)) { return true; }
548  }
549  return CFileUtils::isSwiftInstaller(fileName);
550  }
551 
552  bool CFileUtils::isSwiftInstaller(const QString &fileName)
553  {
554  if (fileName.isEmpty()) { return false; }
555  return fileName.contains("swift", Qt::CaseInsensitive) && fileName.contains("installer");
556  }
557 } // 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.
SWIFT_MISC_EXPORT bool stringCompare(const QString &c1, const QString &c2, Qt::CaseSensitivity cs)
String compare.