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.
34 #include <QDataStream>
38 #include <QMimeDatabase>
39 #include <QRegularExpression>
40 #include <QStringView>
42 #include "base/global.h"
45 const Qt::CaseSensitivity CASE_SENSITIVITY
= Qt::CaseInsensitive
;
47 const Qt::CaseSensitivity CASE_SENSITIVITY
= Qt::CaseSensitive
;
50 const int PATHLIST_TYPEID
= qRegisterMetaType
<PathList
>("PathList");
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
'\\');
60 return hasSeparator
? QDir::cleanPath(path
) : path
;
64 bool hasDriveLetter(const QStringView path
)
66 const QRegularExpression driveLetterRegex
{u
"^[A-Za-z]:/"_qs
};
67 return driveLetterRegex
.match(path
).hasMatch();
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
89 // https://stackoverflow.com/a/31976060
91 QStringView view
= m_pathStr
;
92 if (hasDriveLetter(view
))
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
};
101 const QRegularExpression regex
{u
"\\0"_qs
};
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
':'))
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
':'))
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
'/');
140 if (slashIndex
== 0) // *nix absolute path
141 return createUnchecked(u
"/"_qs
);
144 // should be `c:/` instead of `c:`
145 if (m_pathStr
.at(slashIndex
- 1) == u
':')
146 return createUnchecked(m_pathStr
.left(slashIndex
+ 1));
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)
159 if (slashIndex
== 0) // *nix absolute path
160 return (m_pathStr
.size() == 1) ? Path() : createUnchecked(u
"/"_qs
);
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));
168 return createUnchecked(m_pathStr
.left(slashIndex
));
171 QString
Path::filename() const
173 const int slashIndex
= m_pathStr
.lastIndexOf(u
'/');
174 if (slashIndex
== -1)
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()))
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
243 QString
Path::toString() const
245 return QDir::toNativeSeparators(m_pathStr
);
248 std::filesystem::path
Path::toStdFsPath() const
251 return {data().toStdWString(), std::filesystem::path::format::generic_format
};
253 return {data().toStdString(), std::filesystem::path::format::generic_format
};
257 Path
&Path::operator/=(const Path
&other
)
259 *this = *this / other
;
263 Path
&Path::operator+=(const QStringView str
)
269 Path
Path::commonPath(const Path
&left
, const Path
&right
)
271 if (left
.isEmpty() || right
.isEmpty())
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)
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
)
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)
305 if (rootFolder
.isEmpty())
306 rootFolder
.m_pathStr
= filePathElements
.at(0).toString();
307 else if (rootFolder
.m_pathStr
!= filePathElements
.at(0))
314 void Path::stripRootFolder(PathList
&filePaths
)
316 const Path commonRootFolder
= findRootFolder(filePaths
);
317 if (commonRootFolder
.isEmpty())
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
)
335 path
.m_pathStr
= pathStr
;
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
)
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
)
372 QDataStream
&operator>>(QDataStream
&in
, Path
&path
)
376 path
= Path(pathStr
);
380 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
381 std::size_t qHash(const Path
&key
, const std::size_t seed
)
383 uint
qHash(const Path
&key
, const uint seed
)
386 return ::qHash(key
.data(), seed
);