Sync translations from Transifex and run lupdate
[qBittorrent.git] / src / base / path.cpp
blob0eef0fa20424ae4b126e8040d823d4086988afec
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "path.h"
32 #include <algorithm>
34 #include <QDataStream>
35 #include <QDir>
36 #include <QFileInfo>
37 #include <QList>
38 #include <QMimeDatabase>
39 #include <QRegularExpression>
40 #include <QStringView>
42 #include "base/global.h"
44 #if defined(Q_OS_WIN)
45 const Qt::CaseSensitivity CASE_SENSITIVITY = Qt::CaseInsensitive;
46 #else
47 const Qt::CaseSensitivity CASE_SENSITIVITY = Qt::CaseSensitive;
48 #endif
50 const int PATHLIST_TYPEID = qRegisterMetaType<PathList>("PathList");
52 namespace
54 QString cleanPath(const QString &path)
56 const bool hasSeparator = std::any_of(path.cbegin(), path.cend(), [](const QChar c)
58 return (c == u'/') || (c == u'\\');
59 });
60 return hasSeparator ? QDir::cleanPath(path) : path;
63 #ifdef Q_OS_WIN
64 bool hasDriveLetter(const QStringView path)
66 const QRegularExpression driveLetterRegex {u"^[A-Za-z]:/"_qs};
67 return driveLetterRegex.match(path).hasMatch();
69 #endif
72 Path::Path(const QString &pathStr)
73 : m_pathStr {cleanPath(pathStr)}
77 Path::Path(const std::string &pathStr)
78 : Path(QString::fromStdString(pathStr))
82 bool Path::isValid() const
84 // does not support UNC path
86 if (isEmpty())
87 return false;
89 // https://stackoverflow.com/a/31976060
90 #if defined(Q_OS_WIN)
91 QStringView view = m_pathStr;
92 if (hasDriveLetter(view))
93 view = view.mid(3);
95 // \\37 is using base-8 number system
96 const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_qs};
97 return !regex.match(view).hasMatch();
98 #elif defined(Q_OS_MACOS)
99 const QRegularExpression regex {u"[\\0:]"_qs};
100 #else
101 const QRegularExpression regex {u"\\0"_qs};
102 #endif
103 return !m_pathStr.contains(regex);
106 bool Path::isEmpty() const
108 return m_pathStr.isEmpty();
111 bool Path::isAbsolute() const
113 // `QDir::isAbsolutePath` treats `:` as a path to QResource, so handle it manually
114 if (m_pathStr.startsWith(u':'))
115 return false;
116 return QDir::isAbsolutePath(m_pathStr);
119 bool Path::isRelative() const
121 // `QDir::isRelativePath` treats `:` as a path to QResource, so handle it manually
122 if (m_pathStr.startsWith(u':'))
123 return true;
124 return QDir::isRelativePath(m_pathStr);
127 bool Path::exists() const
129 return !isEmpty() && QFileInfo::exists(m_pathStr);
132 Path Path::rootItem() const
134 // does not support UNC path
136 const int slashIndex = m_pathStr.indexOf(u'/');
137 if (slashIndex < 0)
138 return *this;
140 if (slashIndex == 0) // *nix absolute path
141 return createUnchecked(u"/"_qs);
143 #ifdef Q_OS_WIN
144 // should be `c:/` instead of `c:`
145 if ((slashIndex == 2) && hasDriveLetter(m_pathStr))
146 return createUnchecked(m_pathStr.left(slashIndex + 1));
147 #endif
148 return createUnchecked(m_pathStr.left(slashIndex));
151 Path Path::parentPath() const
153 // does not support UNC path
155 const int slashIndex = m_pathStr.lastIndexOf(u'/');
156 if (slashIndex == -1)
157 return {};
159 if (slashIndex == 0) // *nix absolute path
160 return (m_pathStr.size() == 1) ? Path() : createUnchecked(u"/"_qs);
162 #ifdef Q_OS_WIN
163 // should be `c:/` instead of `c:`
164 // Windows "drive letter" is limited to one alphabet
165 if ((slashIndex == 2) && hasDriveLetter(m_pathStr))
166 return (m_pathStr.size() == 3) ? Path() : createUnchecked(m_pathStr.left(slashIndex + 1));
167 #endif
168 return createUnchecked(m_pathStr.left(slashIndex));
171 QString Path::filename() const
173 const int slashIndex = m_pathStr.lastIndexOf(u'/');
174 if (slashIndex == -1)
175 return m_pathStr;
177 return m_pathStr.mid(slashIndex + 1);
180 QString Path::extension() const
182 const QString suffix = QMimeDatabase().suffixForFileName(m_pathStr);
183 if (!suffix.isEmpty())
184 return (u"." + suffix);
186 const int slashIndex = m_pathStr.lastIndexOf(u'/');
187 const auto filename = QStringView(m_pathStr).mid(slashIndex + 1);
188 const int dotIndex = filename.lastIndexOf(u'.', -2);
189 return ((dotIndex == -1) ? QString() : filename.mid(dotIndex).toString());
192 bool Path::hasExtension(const QStringView ext) const
194 Q_ASSERT(ext.startsWith(u'.') && (ext.size() >= 2));
196 return m_pathStr.endsWith(ext, Qt::CaseInsensitive);
199 bool Path::hasAncestor(const Path &other) const
201 if (other.isEmpty() || (m_pathStr.size() <= other.m_pathStr.size()))
202 return false;
204 return (m_pathStr[other.m_pathStr.size()] == u'/')
205 && m_pathStr.startsWith(other.m_pathStr, CASE_SENSITIVITY);
208 Path Path::relativePathOf(const Path &childPath) const
210 // If both paths are relative, we assume that they have the same base path
211 if (isRelative() && childPath.isRelative())
212 return Path(QDir(QDir::home().absoluteFilePath(m_pathStr)).relativeFilePath(QDir::home().absoluteFilePath(childPath.data())));
214 return Path(QDir(m_pathStr).relativeFilePath(childPath.data()));
217 void Path::removeExtension()
219 m_pathStr.chop(extension().size());
222 Path Path::removedExtension() const
224 return createUnchecked(m_pathStr.chopped(extension().size()));
227 void Path::removeExtension(const QStringView ext)
229 if (hasExtension(ext))
230 m_pathStr.chop(ext.size());
233 Path Path::removedExtension(const QStringView ext) const
235 return (hasExtension(ext) ? createUnchecked(m_pathStr.chopped(ext.size())) : *this);
238 QString Path::data() const
240 return m_pathStr;
243 QString Path::toString() const
245 return QDir::toNativeSeparators(m_pathStr);
248 std::filesystem::path Path::toStdFsPath() const
250 #ifdef Q_OS_WIN
251 return {data().toStdWString(), std::filesystem::path::format::generic_format};
252 #else
253 return {data().toStdString(), std::filesystem::path::format::generic_format};
254 #endif
257 Path &Path::operator/=(const Path &other)
259 *this = *this / other;
260 return *this;
263 Path &Path::operator+=(const QStringView str)
265 *this = *this + str;
266 return *this;
269 Path Path::commonPath(const Path &left, const Path &right)
271 if (left.isEmpty() || right.isEmpty())
272 return {};
274 const QList<QStringView> leftPathItems = QStringView(left.m_pathStr).split(u'/');
275 const QList<QStringView> rightPathItems = QStringView(right.m_pathStr).split(u'/');
276 int commonItemsCount = 0;
277 qsizetype commonPathSize = 0;
278 while ((commonItemsCount < leftPathItems.size()) && (commonItemsCount < rightPathItems.size()))
280 const QStringView leftPathItem = leftPathItems[commonItemsCount];
281 const QStringView rightPathItem = rightPathItems[commonItemsCount];
282 if (leftPathItem.compare(rightPathItem, CASE_SENSITIVITY) != 0)
283 break;
285 ++commonItemsCount;
286 commonPathSize += leftPathItem.size();
289 if (commonItemsCount > 0)
290 commonPathSize += (commonItemsCount - 1); // size of intermediate separators
292 return Path::createUnchecked(left.m_pathStr.left(commonPathSize));
295 Path Path::findRootFolder(const PathList &filePaths)
297 Path rootFolder;
298 for (const Path &filePath : filePaths)
300 const auto filePathElements = QStringView(filePath.m_pathStr).split(u'/');
301 // if at least one file has no root folder, no common root folder exists
302 if (filePathElements.count() <= 1)
303 return {};
305 if (rootFolder.isEmpty())
306 rootFolder.m_pathStr = filePathElements.at(0).toString();
307 else if (rootFolder.m_pathStr != filePathElements.at(0))
308 return {};
311 return rootFolder;
314 void Path::stripRootFolder(PathList &filePaths)
316 const Path commonRootFolder = findRootFolder(filePaths);
317 if (commonRootFolder.isEmpty())
318 return;
320 for (Path &filePath : filePaths)
321 filePath.m_pathStr = filePath.m_pathStr.mid(commonRootFolder.m_pathStr.size() + 1);
324 void Path::addRootFolder(PathList &filePaths, const Path &rootFolder)
326 Q_ASSERT(!rootFolder.isEmpty());
328 for (Path &filePath : filePaths)
329 filePath = rootFolder / filePath;
332 Path Path::createUnchecked(const QString &pathStr)
334 Path path;
335 path.m_pathStr = pathStr;
337 return path;
340 bool operator==(const Path &lhs, const Path &rhs)
342 return (lhs.data().compare(rhs.data(), CASE_SENSITIVITY) == 0);
345 bool operator!=(const Path &lhs, const Path &rhs)
347 return !(lhs == rhs);
350 Path operator/(const Path &lhs, const Path &rhs)
352 if (rhs.isEmpty())
353 return lhs;
355 if (lhs.isEmpty())
356 return rhs;
358 return Path(lhs.m_pathStr + u'/' + rhs.m_pathStr);
361 Path operator+(const Path &lhs, const QStringView rhs)
363 return Path(lhs.data() + rhs);
366 QDataStream &operator<<(QDataStream &out, const Path &path)
368 out << path.data();
369 return out;
372 QDataStream &operator>>(QDataStream &in, Path &path)
374 QString pathStr;
375 in >> pathStr;
376 path = Path(pathStr);
377 return in;
380 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
381 std::size_t qHash(const Path &key, const std::size_t seed)
382 #else
383 uint qHash(const Path &key, const uint seed)
384 #endif
386 return ::qHash(key.data(), seed);