1 /*
2     SPDX-FileCopyrightText: 2012 Milian Wolff <mail@milianw.de>
3     SPDX-FileCopyrightText: 2015 Kevin Funk <kfunk@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7 
8 #include "path.h"
9 #include "debug.h"
10 
11 #include <QStringList>
12 #include <QDebug>
13 
14 #include <language/util/kdevhash.h>
15 
16 #include <algorithm>
17 #include <iterator>
18 
19 using namespace KDevelop;
20 
21 namespace {
22 
isWindowsDriveLetter(const QString & segment)23 inline bool isWindowsDriveLetter(const QString& segment)
24 {
25 #ifdef Q_OS_WIN
26     return segment.size() == 2 && segment.at(0).isLetter() && segment.at(1) == QLatin1Char(':');
27 #else
28     Q_UNUSED(segment);
29     return false;
30 #endif
31 }
32 
isAbsolutePath(const QString & path)33 inline bool isAbsolutePath(const QString& path)
34 {
35     if (path.startsWith(QLatin1Char('/'))) {
36         return true; // Even on Windows: Potentially a path of a remote URL
37     }
38 
39 #ifdef Q_OS_WIN
40     return path.size() >= 2 && path.at(0).isLetter() && path.at(1) == QLatin1Char(':');
41 #else
42     return false;
43 #endif
44 }
45 
46 }
47 
toUrlOrLocalFile(const QUrl & url,QUrl::FormattingOptions options)48 QString KDevelop::toUrlOrLocalFile(const QUrl& url, QUrl::FormattingOptions options)
49 {
50     const auto str = url.toString(options | QUrl::PreferLocalFile);
51 #ifdef Q_OS_WIN
52     // potentially strip leading slash
53     if (url.isLocalFile() && !str.isEmpty() && str[0] == QLatin1Char('/')) {
54         return str.mid(1); // expensive copying, but we'd like toString(...) to properly format everything first
55     }
56 #endif
57     return str;
58 }
59 
Path(const QString & pathOrUrl)60 Path::Path(const QString& pathOrUrl)
61     : Path(QUrl::fromUserInput(pathOrUrl, QString(), QUrl::DefaultResolution))
62 {
63 }
64 
Path(const QUrl & url)65 Path::Path(const QUrl& url)
66 {
67     if (!url.isValid()) {
68         // empty or invalid Path
69         return;
70     }
71     // we do not support urls with:
72     // - fragments
73     // - sub urls
74     // - query
75     // nor do we support relative urls
76     if (url.hasFragment() || url.hasQuery() || url.isRelative() || url.path().isEmpty()) {
77         // invalid
78         qCWarning(UTIL) << "Path::init: invalid/unsupported Path encountered: " <<
79             qPrintable(url.toDisplayString(QUrl::PreferLocalFile));
80         return;
81     }
82 
83     if (!url.isLocalFile()) {
84         // handle remote urls
85         QString urlPrefix = url.scheme() + QLatin1String("://");
86         const QString user = url.userName();
87         if (!user.isEmpty()) {
88             urlPrefix += user + QLatin1Char('@');
89         }
90         urlPrefix += url.host();
91         if (url.port() != -1) {
92             urlPrefix += QLatin1Char(':') + QString::number(url.port());
93         }
94         m_data << urlPrefix;
95     }
96 
97     addPath(url.isLocalFile() ? url.toLocalFile() : url.path());
98 
99     // support for root paths, they are valid but don't really contain any data
100     if (m_data.isEmpty() || (isRemote() && m_data.size() == 1)) {
101         m_data << QString();
102     }
103 }
104 
Path(const Path & other,const QString & child)105 Path::Path(const Path& other, const QString& child)
106     : m_data(other.m_data)
107 {
108     if (isAbsolutePath(child)) {
109         // absolute path: only share the remote part of @p other
110         m_data.resize(isRemote() ? 1 : 0);
111     } else if (!other.isValid() && !child.isEmpty()) {
112         qCWarning(UTIL) << "Path::Path: tried to append relative path " << qPrintable(child) <<
113             " to invalid base";
114         return;
115     }
116     addPath(child);
117 }
118 
generatePathOrUrl(bool onlyPath,bool isLocalFile,const QVector<QString> & data)119 static QString generatePathOrUrl(bool onlyPath, bool isLocalFile, const QVector<QString>& data)
120 {
121     // more or less a copy of QtPrivate::QStringList_join
122     const int size = data.size();
123 
124     if (size == 0) {
125         return QString();
126     }
127 
128     int totalLength = 0;
129     // separators: '/'
130     totalLength += size;
131 
132     // skip Path segment if we only want the path
133     int start = (onlyPath && !isLocalFile) ? 1 : 0;
134 
135     // path and url prefix
136     for (int i = start; i < size; ++i) {
137         totalLength += data.at(i).size();
138     }
139 
140     // build string representation
141     QString res;
142     res.reserve(totalLength);
143 
144 #ifdef Q_OS_WIN
145     if (start == 0 && isLocalFile) {
146         if(!data.at(0).endsWith(QLatin1Char(':'))) {
147             qCWarning(UTIL) << "Path::generatePathOrUrl: Invalid Windows drive encountered (expected C: or similar), got: " <<
148                 qPrintable(data.at(0));
149         }
150         Q_ASSERT(data.at(0).endsWith(QLatin1Char(':'))); // assume something along "C:"
151         res += data.at(0);
152         start++;
153     }
154 #endif
155 
156     for (int i = start; i < size; ++i) {
157         if (i || isLocalFile) {
158             res += QLatin1Char('/');
159         }
160 
161         res += data.at(i);
162     }
163 
164     return res;
165 }
166 
pathOrUrl() const167 QString Path::pathOrUrl() const
168 {
169     return generatePathOrUrl(false, isLocalFile(), m_data);
170 }
171 
path() const172 QString Path::path() const
173 {
174     return generatePathOrUrl(true, isLocalFile(), m_data);
175 }
176 
toLocalFile() const177 QString Path::toLocalFile() const
178 {
179     return isLocalFile() ? path() : QString();
180 }
181 
relativePath(const Path & path) const182 QString Path::relativePath(const Path& path) const
183 {
184     if (!path.isValid()) {
185         return QString();
186     }
187     if (!isValid() || remotePrefix() != path.remotePrefix()) {
188         // different remote destinations or we are invalid, return input as-is
189         return path.pathOrUrl();
190     }
191     // while I'd love to use QUrl::relativePath here, it seems to behave pretty
192     // strangely, and adds unexpected "./" at the start for example
193     // so instead, do it on our own based on _relativePath in kurl.cpp
194     // this should also be more performant I think
195 
196     // Find where they meet
197     int level = isRemote() ? 1 : 0;
198     const int maxLevel = qMin(m_data.count(), path.m_data.count());
199     while (level < maxLevel && m_data.at(level) == path.m_data.at(level)) {
200         ++level;
201     }
202 
203     // Need to go down out of our path to the common branch.
204     // but keep in mind that e.g. '/' paths have an empty name
205     int backwardSegments = m_data.count() - level;
206     if (backwardSegments && level < maxLevel && m_data.at(level).isEmpty()) {
207         --backwardSegments;
208     }
209 
210     // Now up from the common branch to the second path.
211     int forwardSegmentsLength = 0;
212     for (int i = level; i < path.m_data.count(); ++i) {
213         forwardSegmentsLength += path.m_data.at(i).length();
214         // slashes
215         if (i + 1 != path.m_data.count()) {
216             forwardSegmentsLength += 1;
217         }
218     }
219 
220     QString relativePath;
221     relativePath.reserve((backwardSegments * 3) + forwardSegmentsLength);
222     for (int i = 0; i < backwardSegments; ++i) {
223         relativePath.append(QLatin1String("../"));
224     }
225 
226     for (int i = level; i < path.m_data.count(); ++i) {
227         relativePath.append(path.m_data.at(i));
228         if (i + 1 != path.m_data.count()) {
229             relativePath.append(QLatin1Char('/'));
230         }
231     }
232 
233     Q_ASSERT(relativePath.length() == ((backwardSegments * 3) + forwardSegmentsLength));
234 
235     return relativePath;
236 }
237 
isParentPath(const QVector<QString> & parent,const QVector<QString> & child,bool direct)238 static bool isParentPath(const QVector<QString>& parent, const QVector<QString>& child, bool direct)
239 {
240     if (direct && child.size() != parent.size() + 1) {
241         return false;
242     } else if (!direct && child.size() <= parent.size()) {
243         return false;
244     }
245     for (int i = 0; i < parent.size(); ++i) {
246         if (child.at(i) != parent.at(i)) {
247             // support for trailing '/'
248             if (i + 1 == parent.size() && parent.at(i).isEmpty()) {
249                 return true;
250             }
251             // otherwise we take a different branch here
252             return false;
253         }
254     }
255 
256     return true;
257 }
258 
isParentOf(const Path & path) const259 bool Path::isParentOf(const Path& path) const
260 {
261     if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) {
262         return false;
263     }
264     return isParentPath(m_data, path.m_data, false);
265 }
266 
isDirectParentOf(const Path & path) const267 bool Path::isDirectParentOf(const Path& path) const
268 {
269     if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) {
270         return false;
271     }
272     return isParentPath(m_data, path.m_data, true);
273 }
274 
remotePrefix() const275 QString Path::remotePrefix() const
276 {
277     return isRemote() ? m_data.first() : QString();
278 }
279 
compare(const Path & other,Qt::CaseSensitivity cs) const280 int Path::compare(const Path& other, Qt::CaseSensitivity cs) const
281 {
282     const int size = m_data.size();
283     const int otherSize = other.m_data.size();
284     const int toCompare = std::min(size, otherSize);
285 
286     // compare each Path segment in turn and try to return early
287     for (int i = 0; i < toCompare; ++i) {
288         const int comparison = m_data.at(i).compare(other.m_data.at(i), cs);
289         if (comparison != 0) {
290             return comparison;
291         }
292     }
293 
294     // when we reach this point, all elements that we compared where equal
295     // thus return the segment count difference between the two paths
296     return size - otherSize;
297 }
298 
toUrl() const299 QUrl Path::toUrl() const
300 {
301     return QUrl::fromUserInput(pathOrUrl());
302 }
303 
isLocalFile() const304 bool Path::isLocalFile() const
305 {
306     // if the first data element contains a '/' it is a Path prefix
307     return !m_data.isEmpty() && !m_data.first().contains(QLatin1Char('/'));
308 }
309 
isRemote() const310 bool Path::isRemote() const
311 {
312     return !m_data.isEmpty() && m_data.first().contains(QLatin1Char('/'));
313 }
314 
lastPathSegment() const315 QString Path::lastPathSegment() const
316 {
317     // remote Paths are offset by one, thus never return the first item of them as file name
318     if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) {
319         return QString();
320     }
321     return m_data.last();
322 }
323 
setLastPathSegment(const QString & name)324 void Path::setLastPathSegment(const QString& name)
325 {
326     // remote Paths are offset by one, thus never return the first item of them as file name
327     if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) {
328         // append the name to empty Paths or remote Paths only containing the Path prefix
329         m_data.append(name);
330     } else {
331         // overwrite the last data member
332         m_data.last() = name;
333     }
334 }
335 
cleanPath(QVector<QString> * data,const bool isRemote)336 static void cleanPath(QVector<QString>* data, const bool isRemote)
337 {
338     if (data->isEmpty()) {
339         return;
340     }
341     const int startOffset = isRemote ? 1 : 0;
342     const auto start = data->begin() + startOffset;
343 
344     auto it = start;
345     while (it != data->end()) {
346         if (*it == QLatin1String("..")) {
347             if (it == start) {
348                 it = data->erase(it);
349             } else {
350                 if (isWindowsDriveLetter(*(it - 1))) {
351                     it = data->erase(it); // keep the drive letter
352                 } else {
353                     it = data->erase(it - 1, it + 1);
354                 }
355             }
356         } else if (*it == QLatin1String(".")) {
357             it = data->erase(it);
358         } else {
359             ++it;
360         }
361     }
362     if (data->count() == startOffset) {
363         data->append(QString());
364     }
365 }
366 
367 // Optimized QString::split code for the specific Path use-case
splitPath(const QString & source)368 static QVarLengthArray<QString, 16> splitPath(const QString& source)
369 {
370     QVarLengthArray<QString, 16> list;
371     int start = 0;
372     int end = 0;
373     while ((end = source.indexOf(QLatin1Char('/'), start)) != -1) {
374         if (start != end) {
375             list.append(source.mid(start, end - start));
376         }
377         start = end + 1;
378     }
379     if (start != source.size()) {
380         list.append(source.mid(start, -1));
381     }
382     return list;
383 }
384 
addPath(const QString & path)385 void Path::addPath(const QString& path)
386 {
387     if (path.isEmpty()) {
388         return;
389     }
390 
391     const auto& newData = splitPath(path);
392     if (newData.isEmpty()) {
393         if (m_data.size() == (isRemote() ? 1 : 0)) {
394             // this represents the root path, we just turned an invalid path into it
395             m_data << QString();
396         }
397         return;
398     }
399 
400     auto it = newData.begin();
401     if (!m_data.isEmpty() && m_data.last().isEmpty()) {
402         // the root item is empty, set its contents and continue appending
403         m_data.last() = *it;
404         ++it;
405     }
406 
407     std::copy(it, newData.end(), std::back_inserter(m_data));
408     cleanPath(&m_data, isRemote());
409 }
410 
parent() const411 Path Path::parent() const
412 {
413     if (m_data.isEmpty()) {
414         return Path();
415     }
416 
417     Path ret(*this);
418     if (m_data.size() == (1 + (isRemote() ? 1 : 0))) {
419         // keep the root item, but clear it, otherwise we'd make the path invalid
420         // or a URL a local path
421         auto& root = ret.m_data.last();
422         if (!isWindowsDriveLetter(root)) {
423             root.clear();
424         }
425     } else {
426         ret.m_data.pop_back();
427     }
428     return ret;
429 }
430 
hasParent() const431 bool Path::hasParent() const
432 {
433     const int rootIdx = isRemote() ? 1 : 0;
434     return m_data.size() > rootIdx && !m_data[rootIdx].isEmpty();
435 }
436 
cd(const QString & dir) const437 Path Path::cd(const QString& dir) const
438 {
439     if (!isValid()) {
440         return Path();
441     }
442     return Path(*this, dir);
443 }
444 
445 namespace KDevelop {
qHash(const Path & path)446 uint qHash(const Path& path)
447 {
448     KDevHash hash;
449     for (const QString& segment : path.segments()) {
450         hash << qHash(segment);
451     }
452 
453     return hash;
454 }
455 
456 template<typename Container>
toPathList_impl(const Container & list)457 static Path::List toPathList_impl(const Container& list)
458 {
459     Path::List ret;
460     ret.reserve(list.size());
461     for (const auto& entry : list) {
462         Path path(entry);
463         if (path.isValid()) {
464             ret << path;
465         }
466     }
467 
468     ret.squeeze();
469     return ret;
470 }
471 
toPathList(const QList<QUrl> & list)472 Path::List toPathList(const QList<QUrl>& list)
473 {
474     return toPathList_impl(list);
475 }
476 
toPathList(const QList<QString> & list)477 Path::List toPathList(const QList<QString>& list)
478 {
479     return toPathList_impl(list);
480 }
481 
482 }
483 
operator <<(QDebug s,const Path & string)484 QDebug operator<<(QDebug s, const Path& string)
485 {
486     s.nospace() << string.pathOrUrl();
487     return s.space();
488 }
489 
490 namespace QTest {
491 template<>
toString(const Path & path)492 char* toString(const Path& path)
493 {
494     return qstrdup(qPrintable(path.pathOrUrl()));
495 }
496 }
497