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 }