File indexing completed on 2024-05-05 04:38:46

0001 /*
0002     SPDX-FileCopyrightText: 2012 Milian Wolff <mail@milianw.de>
0003     SPDX-FileCopyrightText: 2015 Kevin Funk <kfunk@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "path.h"
0009 #include "debug.h"
0010 
0011 #include <QStringList>
0012 #include <QDebug>
0013 
0014 #include <language/util/kdevhash.h>
0015 
0016 #include <algorithm>
0017 #include <iterator>
0018 
0019 using namespace KDevelop;
0020 
0021 namespace {
0022 
0023 inline bool isWindowsDriveLetter(const QString& segment)
0024 {
0025 #ifdef Q_OS_WIN
0026     return segment.size() == 2 && segment.at(0).isLetter() && segment.at(1) == QLatin1Char(':');
0027 #else
0028     Q_UNUSED(segment);
0029     return false;
0030 #endif
0031 }
0032 
0033 inline bool isAbsolutePath(const QString& path)
0034 {
0035     if (path.startsWith(QLatin1Char('/'))) {
0036         return true; // Even on Windows: Potentially a path of a remote URL
0037     }
0038 
0039 #ifdef Q_OS_WIN
0040     return path.size() >= 2 && path.at(0).isLetter() && path.at(1) == QLatin1Char(':');
0041 #else
0042     return false;
0043 #endif
0044 }
0045 
0046 }
0047 
0048 QString KDevelop::toUrlOrLocalFile(const QUrl& url, QUrl::FormattingOptions options)
0049 {
0050     const auto str = url.toString(options | QUrl::PreferLocalFile);
0051 #ifdef Q_OS_WIN
0052     // potentially strip leading slash
0053     if (url.isLocalFile() && !str.isEmpty() && str[0] == QLatin1Char('/')) {
0054         return str.mid(1); // expensive copying, but we'd like toString(...) to properly format everything first
0055     }
0056 #endif
0057     return str;
0058 }
0059 
0060 Path::Path(const QString& pathOrUrl)
0061     : Path(QUrl::fromUserInput(pathOrUrl, QString(), QUrl::DefaultResolution))
0062 {
0063 }
0064 
0065 Path::Path(const QUrl& url)
0066 {
0067     if (!url.isValid()) {
0068         // empty or invalid Path
0069         return;
0070     }
0071     // we do not support urls with:
0072     // - fragments
0073     // - sub urls
0074     // - query
0075     // nor do we support relative urls
0076     if (url.hasFragment() || url.hasQuery() || url.isRelative() || url.path().isEmpty()) {
0077         // invalid
0078         qCWarning(UTIL) << "Path::init: invalid/unsupported Path encountered: " <<
0079             qPrintable(url.toDisplayString(QUrl::PreferLocalFile));
0080         return;
0081     }
0082 
0083     if (!url.isLocalFile()) {
0084         // handle remote urls
0085         QString urlPrefix = url.scheme() + QLatin1String("://");
0086         const QString user = url.userName();
0087         if (!user.isEmpty()) {
0088             urlPrefix += user + QLatin1Char('@');
0089         }
0090         urlPrefix += url.host();
0091         if (url.port() != -1) {
0092             urlPrefix += QLatin1Char(':') + QString::number(url.port());
0093         }
0094         m_data << urlPrefix;
0095     }
0096 
0097     addPath(url.isLocalFile() ? url.toLocalFile() : url.path());
0098 
0099     // support for root paths, they are valid but don't really contain any data
0100     if (m_data.isEmpty() || (isRemote() && m_data.size() == 1)) {
0101         m_data << QString();
0102     }
0103 }
0104 
0105 Path::Path(const Path& other, const QString& child)
0106     : m_data(other.m_data)
0107 {
0108     if (isAbsolutePath(child)) {
0109         // absolute path: only share the remote part of @p other
0110         m_data.resize(isRemote() ? 1 : 0);
0111     } else if (!other.isValid() && !child.isEmpty()) {
0112         qCWarning(UTIL) << "Path::Path: tried to append relative path " << qPrintable(child) <<
0113             " to invalid base";
0114         return;
0115     }
0116     addPath(child);
0117 }
0118 
0119 static QString generatePathOrUrl(bool onlyPath, bool isLocalFile, const QVector<QString>& data)
0120 {
0121     // more or less a copy of QtPrivate::QStringList_join
0122     const int size = data.size();
0123 
0124     if (size == 0) {
0125         return QString();
0126     }
0127 
0128     int totalLength = 0;
0129     // separators: '/'
0130     totalLength += size;
0131 
0132     // skip Path segment if we only want the path
0133     int start = (onlyPath && !isLocalFile) ? 1 : 0;
0134 
0135     // path and url prefix
0136     for (int i = start; i < size; ++i) {
0137         totalLength += data.at(i).size();
0138     }
0139 
0140     // build string representation
0141     QString res;
0142     res.reserve(totalLength);
0143 
0144 #ifdef Q_OS_WIN
0145     if (start == 0 && isLocalFile) {
0146         if(!data.at(0).endsWith(QLatin1Char(':'))) {
0147             qCWarning(UTIL) << "Path::generatePathOrUrl: Invalid Windows drive encountered (expected C: or similar), got: " <<
0148                 qPrintable(data.at(0));
0149         }
0150         Q_ASSERT(data.at(0).endsWith(QLatin1Char(':'))); // assume something along "C:"
0151         res += data.at(0);
0152         start++;
0153     }
0154 #endif
0155 
0156     for (int i = start; i < size; ++i) {
0157         if (i || isLocalFile) {
0158             res += QLatin1Char('/');
0159         }
0160 
0161         res += data.at(i);
0162     }
0163 
0164     return res;
0165 }
0166 
0167 QString Path::pathOrUrl() const
0168 {
0169     return generatePathOrUrl(false, isLocalFile(), m_data);
0170 }
0171 
0172 QString Path::path() const
0173 {
0174     return generatePathOrUrl(true, isLocalFile(), m_data);
0175 }
0176 
0177 QString Path::toLocalFile() const
0178 {
0179     return isLocalFile() ? path() : QString();
0180 }
0181 
0182 QString Path::relativePath(const Path& path) const
0183 {
0184     if (!path.isValid()) {
0185         return QString();
0186     }
0187     if (!isValid() || remotePrefix() != path.remotePrefix()) {
0188         // different remote destinations or we are invalid, return input as-is
0189         return path.pathOrUrl();
0190     }
0191     // while I'd love to use QUrl::relativePath here, it seems to behave pretty
0192     // strangely, and adds unexpected "./" at the start for example
0193     // so instead, do it on our own based on _relativePath in kurl.cpp
0194     // this should also be more performant I think
0195 
0196     // Find where they meet
0197     int level = isRemote() ? 1 : 0;
0198     const int maxLevel = qMin(m_data.count(), path.m_data.count());
0199     while (level < maxLevel && m_data.at(level) == path.m_data.at(level)) {
0200         ++level;
0201     }
0202 
0203     // Need to go down out of our path to the common branch.
0204     // but keep in mind that e.g. '/' paths have an empty name
0205     int backwardSegments = m_data.count() - level;
0206     if (backwardSegments && level < maxLevel && m_data.at(level).isEmpty()) {
0207         --backwardSegments;
0208     }
0209 
0210     // Now up from the common branch to the second path.
0211     int forwardSegmentsLength = 0;
0212     for (int i = level; i < path.m_data.count(); ++i) {
0213         forwardSegmentsLength += path.m_data.at(i).length();
0214         // slashes
0215         if (i + 1 != path.m_data.count()) {
0216             forwardSegmentsLength += 1;
0217         }
0218     }
0219 
0220     QString relativePath;
0221     relativePath.reserve((backwardSegments * 3) + forwardSegmentsLength);
0222     for (int i = 0; i < backwardSegments; ++i) {
0223         relativePath.append(QLatin1String("../"));
0224     }
0225 
0226     for (int i = level; i < path.m_data.count(); ++i) {
0227         relativePath.append(path.m_data.at(i));
0228         if (i + 1 != path.m_data.count()) {
0229             relativePath.append(QLatin1Char('/'));
0230         }
0231     }
0232 
0233     Q_ASSERT(relativePath.length() == ((backwardSegments * 3) + forwardSegmentsLength));
0234 
0235     return relativePath;
0236 }
0237 
0238 static bool isParentPath(const QVector<QString>& parent, const QVector<QString>& child, bool direct)
0239 {
0240     if (direct && child.size() != parent.size() + 1) {
0241         return false;
0242     } else if (!direct && child.size() <= parent.size()) {
0243         return false;
0244     }
0245     for (int i = 0; i < parent.size(); ++i) {
0246         if (child.at(i) != parent.at(i)) {
0247             // support for trailing '/'
0248             if (i + 1 == parent.size() && parent.at(i).isEmpty()) {
0249                 return true;
0250             }
0251             // otherwise we take a different branch here
0252             return false;
0253         }
0254     }
0255 
0256     return true;
0257 }
0258 
0259 bool Path::isParentOf(const Path& path) const
0260 {
0261     if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) {
0262         return false;
0263     }
0264     return isParentPath(m_data, path.m_data, false);
0265 }
0266 
0267 bool Path::isDirectParentOf(const Path& path) const
0268 {
0269     if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) {
0270         return false;
0271     }
0272     return isParentPath(m_data, path.m_data, true);
0273 }
0274 
0275 QString Path::remotePrefix() const
0276 {
0277     return isRemote() ? m_data.first() : QString();
0278 }
0279 
0280 int Path::compare(const Path& other, Qt::CaseSensitivity cs) const
0281 {
0282     const int size = m_data.size();
0283     const int otherSize = other.m_data.size();
0284     const int toCompare = std::min(size, otherSize);
0285 
0286     // compare each Path segment in turn and try to return early
0287     for (int i = 0; i < toCompare; ++i) {
0288         const int comparison = m_data.at(i).compare(other.m_data.at(i), cs);
0289         if (comparison != 0) {
0290             return comparison;
0291         }
0292     }
0293 
0294     // when we reach this point, all elements that we compared where equal
0295     // thus return the segment count difference between the two paths
0296     return size - otherSize;
0297 }
0298 
0299 QUrl Path::toUrl() const
0300 {
0301     return QUrl::fromUserInput(pathOrUrl());
0302 }
0303 
0304 bool Path::isLocalFile() const
0305 {
0306     // if the first data element contains a '/' it is a Path prefix
0307     return !m_data.isEmpty() && !m_data.first().contains(QLatin1Char('/'));
0308 }
0309 
0310 bool Path::isRemote() const
0311 {
0312     return !m_data.isEmpty() && m_data.first().contains(QLatin1Char('/'));
0313 }
0314 
0315 QString Path::lastPathSegment() const
0316 {
0317     // remote Paths are offset by one, thus never return the first item of them as file name
0318     if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) {
0319         return QString();
0320     }
0321     return m_data.last();
0322 }
0323 
0324 void Path::setLastPathSegment(const QString& name)
0325 {
0326     // remote Paths are offset by one, thus never return the first item of them as file name
0327     if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) {
0328         // append the name to empty Paths or remote Paths only containing the Path prefix
0329         m_data.append(name);
0330     } else {
0331         // overwrite the last data member
0332         m_data.last() = name;
0333     }
0334 }
0335 
0336 static void cleanPath(QVector<QString>* data, const bool isRemote)
0337 {
0338     if (data->isEmpty()) {
0339         return;
0340     }
0341     const int startOffset = isRemote ? 1 : 0;
0342     const auto start = data->begin() + startOffset;
0343 
0344     auto it = start;
0345     while (it != data->end()) {
0346         if (*it == QLatin1String("..")) {
0347             if (it == start) {
0348                 it = data->erase(it);
0349             } else {
0350                 if (isWindowsDriveLetter(*(it - 1))) {
0351                     it = data->erase(it); // keep the drive letter
0352                 } else {
0353                     it = data->erase(it - 1, it + 1);
0354                 }
0355             }
0356         } else if (*it == QLatin1String(".")) {
0357             it = data->erase(it);
0358         } else {
0359             ++it;
0360         }
0361     }
0362     if (data->count() == startOffset) {
0363         data->append(QString());
0364     }
0365 }
0366 
0367 // Optimized QString::split code for the specific Path use-case
0368 static QVarLengthArray<QString, 16> splitPath(const QString& source)
0369 {
0370     QVarLengthArray<QString, 16> list;
0371     int start = 0;
0372     int end = 0;
0373     while ((end = source.indexOf(QLatin1Char('/'), start)) != -1) {
0374         if (start != end) {
0375             list.append(source.mid(start, end - start));
0376         }
0377         start = end + 1;
0378     }
0379     if (start != source.size()) {
0380         list.append(source.mid(start, -1));
0381     }
0382     return list;
0383 }
0384 
0385 void Path::addPath(const QString& path)
0386 {
0387     if (path.isEmpty()) {
0388         return;
0389     }
0390 
0391     const auto& newData = splitPath(path);
0392     if (newData.isEmpty()) {
0393         if (m_data.size() == (isRemote() ? 1 : 0)) {
0394             // this represents the root path, we just turned an invalid path into it
0395             m_data << QString();
0396         }
0397         return;
0398     }
0399 
0400     auto it = newData.begin();
0401     if (!m_data.isEmpty() && m_data.last().isEmpty()) {
0402         // the root item is empty, set its contents and continue appending
0403         m_data.last() = *it;
0404         ++it;
0405     }
0406 
0407     std::copy(it, newData.end(), std::back_inserter(m_data));
0408     cleanPath(&m_data, isRemote());
0409 }
0410 
0411 Path Path::parent() const
0412 {
0413     if (m_data.isEmpty()) {
0414         return Path();
0415     }
0416 
0417     Path ret(*this);
0418     if (m_data.size() == (1 + (isRemote() ? 1 : 0))) {
0419         // keep the root item, but clear it, otherwise we'd make the path invalid
0420         // or a URL a local path
0421         auto& root = ret.m_data.last();
0422         if (!isWindowsDriveLetter(root)) {
0423             root.clear();
0424         }
0425     } else {
0426         ret.m_data.pop_back();
0427     }
0428     return ret;
0429 }
0430 
0431 bool Path::hasParent() const
0432 {
0433     const int rootIdx = isRemote() ? 1 : 0;
0434     return m_data.size() > rootIdx && !m_data[rootIdx].isEmpty();
0435 }
0436 
0437 Path Path::cd(const QString& dir) const
0438 {
0439     if (!isValid()) {
0440         return Path();
0441     }
0442     return Path(*this, dir);
0443 }
0444 
0445 namespace KDevelop {
0446 uint qHash(const Path& path)
0447 {
0448     KDevHash hash;
0449     for (const QString& segment : path.segments()) {
0450         hash << qHash(segment);
0451     }
0452 
0453     return hash;
0454 }
0455 
0456 template<typename Container>
0457 static Path::List toPathList_impl(const Container& list)
0458 {
0459     Path::List ret;
0460     ret.reserve(list.size());
0461     for (const auto& entry : list) {
0462         Path path(entry);
0463         if (path.isValid()) {
0464             ret << path;
0465         }
0466     }
0467 
0468     ret.squeeze();
0469     return ret;
0470 }
0471 
0472 Path::List toPathList(const QList<QUrl>& list)
0473 {
0474     return toPathList_impl(list);
0475 }
0476 
0477 Path::List toPathList(const QList<QString>& list)
0478 {
0479     return toPathList_impl(list);
0480 }
0481 
0482 }
0483 
0484 QDebug operator<<(QDebug s, const Path& string)
0485 {
0486     s.nospace() << string.pathOrUrl();
0487     return s.space();
0488 }
0489 
0490 namespace QTest {
0491 template<>
0492 char* toString(const Path& path)
0493 {
0494     return qstrdup(qPrintable(path.pathOrUrl()));
0495 }
0496 }