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/concepts/stringable.h"
43 #include "base/global.h"
46 const Qt::CaseSensitivity CASE_SENSITIVITY
= Qt::CaseInsensitive
;
48 const Qt::CaseSensitivity CASE_SENSITIVITY
= Qt::CaseSensitive
;
51 const int PATHLIST_TYPEID
= qRegisterMetaType
<PathList
>("PathList");
55 QString
cleanPath(const QString
&path
)
57 const bool hasSeparator
= std::any_of(path
.cbegin(), path
.cend(), [](const QChar c
)
59 return (c
== u
'/') || (c
== u
'\\');
61 return hasSeparator
? QDir::cleanPath(path
) : path
;
65 bool hasDriveLetter(const QStringView path
)
67 const QRegularExpression driveLetterRegex
{u
"^[A-Za-z]:/"_s
};
68 return driveLetterRegex
.match(path
).hasMatch();
73 // `Path` should satisfy `Stringable` concept in order to be stored in settings as string
74 static_assert(Stringable
<Path
>);
76 Path::Path(const QString
&pathStr
)
77 : m_pathStr
{cleanPath(pathStr
)}
81 Path::Path(const std::string
&pathStr
)
82 : Path(QString::fromStdString(pathStr
))
86 bool Path::isValid() const
88 // does not support UNC path
93 // https://stackoverflow.com/a/31976060
95 QStringView view
= m_pathStr
;
96 if (hasDriveLetter(view
))
99 // \\37 is using base-8 number system
100 const QRegularExpression regex
{u
"[\\0-\\37:?\"*<>|]"_s
};
101 return !regex
.match(view
).hasMatch();
102 #elif defined(Q_OS_MACOS)
103 const QRegularExpression regex
{u
"[\\0:]"_s
};
105 const QRegularExpression regex
{u
"\\0"_s
};
107 return !m_pathStr
.contains(regex
);
110 bool Path::isEmpty() const
112 return m_pathStr
.isEmpty();
115 bool Path::isAbsolute() const
117 // `QDir::isAbsolutePath` treats `:` as a path to QResource, so handle it manually
118 if (m_pathStr
.startsWith(u
':'))
120 return QDir::isAbsolutePath(m_pathStr
);
123 bool Path::isRelative() const
125 // `QDir::isRelativePath` treats `:` as a path to QResource, so handle it manually
126 if (m_pathStr
.startsWith(u
':'))
128 return QDir::isRelativePath(m_pathStr
);
131 bool Path::exists() const
133 return !isEmpty() && QFileInfo::exists(m_pathStr
);
136 Path
Path::rootItem() const
138 // does not support UNC path
140 const int slashIndex
= m_pathStr
.indexOf(u
'/');
144 if (slashIndex
== 0) // *nix absolute path
145 return createUnchecked(u
"/"_s
);
148 // should be `c:/` instead of `c:`
149 if ((slashIndex
== 2) && hasDriveLetter(m_pathStr
))
150 return createUnchecked(m_pathStr
.left(slashIndex
+ 1));
152 return createUnchecked(m_pathStr
.left(slashIndex
));
155 Path
Path::parentPath() const
157 // does not support UNC path
159 const int slashIndex
= m_pathStr
.lastIndexOf(u
'/');
160 if (slashIndex
== -1)
163 if (slashIndex
== 0) // *nix absolute path
164 return (m_pathStr
.size() == 1) ? Path() : createUnchecked(u
"/"_s
);
167 // should be `c:/` instead of `c:`
168 // Windows "drive letter" is limited to one alphabet
169 if ((slashIndex
== 2) && hasDriveLetter(m_pathStr
))
170 return (m_pathStr
.size() == 3) ? Path() : createUnchecked(m_pathStr
.left(slashIndex
+ 1));
172 return createUnchecked(m_pathStr
.left(slashIndex
));
175 QString
Path::filename() const
177 const int slashIndex
= m_pathStr
.lastIndexOf(u
'/');
178 if (slashIndex
== -1)
181 return m_pathStr
.mid(slashIndex
+ 1);
184 QString
Path::extension() const
186 const QString suffix
= QMimeDatabase().suffixForFileName(m_pathStr
);
187 if (!suffix
.isEmpty())
188 return (u
"." + suffix
);
190 const int slashIndex
= m_pathStr
.lastIndexOf(u
'/');
191 const auto filename
= QStringView(m_pathStr
).mid(slashIndex
+ 1);
192 const int dotIndex
= filename
.lastIndexOf(u
'.', -2);
193 return ((dotIndex
== -1) ? QString() : filename
.mid(dotIndex
).toString());
196 bool Path::hasExtension(const QStringView ext
) const
198 Q_ASSERT(ext
.startsWith(u
'.') && (ext
.size() >= 2));
200 return m_pathStr
.endsWith(ext
, Qt::CaseInsensitive
);
203 bool Path::hasAncestor(const Path
&other
) const
205 if (other
.isEmpty() || (m_pathStr
.size() <= other
.m_pathStr
.size()))
208 return (m_pathStr
[other
.m_pathStr
.size()] == u
'/')
209 && m_pathStr
.startsWith(other
.m_pathStr
, CASE_SENSITIVITY
);
212 Path
Path::relativePathOf(const Path
&childPath
) const
214 // If both paths are relative, we assume that they have the same base path
215 if (isRelative() && childPath
.isRelative())
216 return Path(QDir(QDir::home().absoluteFilePath(m_pathStr
)).relativeFilePath(QDir::home().absoluteFilePath(childPath
.data())));
218 return Path(QDir(m_pathStr
).relativeFilePath(childPath
.data()));
221 void Path::removeExtension()
223 m_pathStr
.chop(extension().size());
226 Path
Path::removedExtension() const
228 return createUnchecked(m_pathStr
.chopped(extension().size()));
231 void Path::removeExtension(const QStringView ext
)
233 if (hasExtension(ext
))
234 m_pathStr
.chop(ext
.size());
237 Path
Path::removedExtension(const QStringView ext
) const
239 return (hasExtension(ext
) ? createUnchecked(m_pathStr
.chopped(ext
.size())) : *this);
242 QString
Path::data() const
247 QString
Path::toString() const
249 return QDir::toNativeSeparators(m_pathStr
);
252 std::filesystem::path
Path::toStdFsPath() const
255 return {data().toStdWString(), std::filesystem::path::format::generic_format
};
257 return {data().toStdString(), std::filesystem::path::format::generic_format
};
261 Path
&Path::operator/=(const Path
&other
)
263 *this = *this / other
;
267 Path
&Path::operator+=(const QStringView str
)
273 Path
Path::commonPath(const Path
&left
, const Path
&right
)
275 if (left
.isEmpty() || right
.isEmpty())
278 const QList
<QStringView
> leftPathItems
= QStringView(left
.m_pathStr
).split(u
'/');
279 const QList
<QStringView
> rightPathItems
= QStringView(right
.m_pathStr
).split(u
'/');
280 int commonItemsCount
= 0;
281 qsizetype commonPathSize
= 0;
282 while ((commonItemsCount
< leftPathItems
.size()) && (commonItemsCount
< rightPathItems
.size()))
284 const QStringView leftPathItem
= leftPathItems
[commonItemsCount
];
285 const QStringView rightPathItem
= rightPathItems
[commonItemsCount
];
286 if (leftPathItem
.compare(rightPathItem
, CASE_SENSITIVITY
) != 0)
290 commonPathSize
+= leftPathItem
.size();
293 if (commonItemsCount
> 0)
294 commonPathSize
+= (commonItemsCount
- 1); // size of intermediate separators
296 return Path::createUnchecked(left
.m_pathStr
.left(commonPathSize
));
299 Path
Path::findRootFolder(const PathList
&filePaths
)
302 for (const Path
&filePath
: filePaths
)
304 const auto filePathElements
= QStringView(filePath
.m_pathStr
).split(u
'/');
305 // if at least one file has no root folder, no common root folder exists
306 if (filePathElements
.count() <= 1)
309 if (rootFolder
.isEmpty())
310 rootFolder
.m_pathStr
= filePathElements
.at(0).toString();
311 else if (rootFolder
.m_pathStr
!= filePathElements
.at(0))
318 void Path::stripRootFolder(PathList
&filePaths
)
320 const Path commonRootFolder
= findRootFolder(filePaths
);
321 if (commonRootFolder
.isEmpty())
324 for (Path
&filePath
: filePaths
)
325 filePath
.m_pathStr
.remove(0, (commonRootFolder
.m_pathStr
.size() + 1));
328 void Path::addRootFolder(PathList
&filePaths
, const Path
&rootFolder
)
330 Q_ASSERT(!rootFolder
.isEmpty());
332 for (Path
&filePath
: filePaths
)
333 filePath
= rootFolder
/ filePath
;
336 Path
Path::createUnchecked(const QString
&pathStr
)
339 path
.m_pathStr
= pathStr
;
344 bool operator==(const Path
&lhs
, const Path
&rhs
)
346 return (lhs
.data().compare(rhs
.data(), CASE_SENSITIVITY
) == 0);
349 Path
operator/(const Path
&lhs
, const Path
&rhs
)
357 return Path(lhs
.m_pathStr
+ u
'/' + rhs
.m_pathStr
);
360 Path
operator+(const Path
&lhs
, const QStringView rhs
)
362 return Path(lhs
.data() + rhs
);
365 QDataStream
&operator<<(QDataStream
&out
, const Path
&path
)
371 QDataStream
&operator>>(QDataStream
&in
, Path
&path
)
375 path
= Path(pathStr
);
379 std::size_t qHash(const Path
&key
, const std::size_t seed
)
381 return ::qHash(key
.data(), seed
);