File: path.cpp

package info (click to toggle)
kdevelop 4%3A5.6.2-4
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 57,892 kB
  • sloc: cpp: 278,773; javascript: 3,558; python: 3,385; sh: 1,317; ansic: 689; xml: 273; php: 95; makefile: 40; lisp: 13; sed: 12
file content (523 lines) | stat: -rw-r--r-- 14,131 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
/*
 * This file is part of KDevelop
 * Copyright 2012 Milian Wolff <mail@milianw.de>
 * Copyright 2015 Kevin Funk <kfunk@kde.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3, or any
 * later version accepted by the membership of KDE e.V. (or its
 * successor approved by the membership of KDE e.V.), which shall
 * act as a proxy defined in Section 6 of version 3 of the license.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "path.h"
#include "debug.h"

#include <QStringList>
#include <QDebug>

#include <language/util/kdevhash.h>

using namespace KDevelop;

namespace {

inline bool isWindowsDriveLetter(const QString& segment)
{
#ifdef Q_OS_WIN
    return segment.size() == 2 && segment.at(0).isLetter() && segment.at(1) == QLatin1Char(':');
#else
    Q_UNUSED(segment);
    return false;
#endif
}

inline bool isAbsolutePath(const QString& path)
{
    if (path.startsWith(QLatin1Char('/'))) {
        return true; // Even on Windows: Potentially a path of a remote URL
    }

#ifdef Q_OS_WIN
    return path.size() >= 2 && path.at(0).isLetter() && path.at(1) == QLatin1Char(':');
#else
    return false;
#endif
}

}

QString KDevelop::toUrlOrLocalFile(const QUrl& url, QUrl::FormattingOptions options)
{
    const auto str = url.toString(options | QUrl::PreferLocalFile);
#ifdef Q_OS_WIN
    // potentially strip leading slash
    if (url.isLocalFile() && !str.isEmpty() && str[0] == QLatin1Char('/')) {
        return str.mid(1); // expensive copying, but we'd like toString(...) to properly format everything first
    }
#endif
    return str;
}

Path::Path()
{

}

Path::Path(const QString& pathOrUrl)
    : Path(QUrl::fromUserInput(pathOrUrl, QString(), QUrl::DefaultResolution))
{
}

Path::Path(const QUrl& url)
{
    if (!url.isValid()) {
        // empty or invalid Path
        return;
    }
    // we do not support urls with:
    // - fragments
    // - sub urls
    // - query
    // nor do we support relative urls
    if (url.hasFragment() || url.hasQuery() || url.isRelative() || url.path().isEmpty()) {
        // invalid
        qCWarning(UTIL) << "Path::init: invalid/unsupported Path encountered: " <<
            qPrintable(url.toDisplayString(QUrl::PreferLocalFile));
        return;
    }

    if (!url.isLocalFile()) {
        // handle remote urls
        QString urlPrefix = url.scheme() + QLatin1String("://");
        const QString user = url.userName();
        if (!user.isEmpty()) {
            urlPrefix += user + QLatin1Char('@');
        }
        urlPrefix += url.host();
        if (url.port() != -1) {
            urlPrefix += QLatin1Char(':') + QString::number(url.port());
        }
        m_data << urlPrefix;
    }

    addPath(url.isLocalFile() ? url.toLocalFile() : url.path());

    // support for root paths, they are valid but don't really contain any data
    if (m_data.isEmpty() || (isRemote() && m_data.size() == 1)) {
        m_data << QString();
    }
}

Path::Path(const Path& other, const QString& child)
    : m_data(other.m_data)
{
    if (isAbsolutePath(child)) {
        // absolute path: only share the remote part of @p other
        m_data.resize(isRemote() ? 1 : 0);
    } else if (!other.isValid() && !child.isEmpty()) {
        qCWarning(UTIL) << "Path::Path: tried to append relative path " << qPrintable(child) <<
            " to invalid base";
        return;
    }
    addPath(child);
}

static QString generatePathOrUrl(bool onlyPath, bool isLocalFile, const QVector<QString>& data)
{
    // more or less a copy of QtPrivate::QStringList_join
    const int size = data.size();

    if (size == 0) {
        return QString();
    }

    int totalLength = 0;
    // separators: '/'
    totalLength += size;

    // skip Path segment if we only want the path
    int start = (onlyPath && !isLocalFile) ? 1 : 0;

    // path and url prefix
    for (int i = start; i < size; ++i) {
        totalLength += data.at(i).size();
    }

    // build string representation
    QString res;
    res.reserve(totalLength);

#ifdef Q_OS_WIN
    if (start == 0 && isLocalFile) {
        if(!data.at(0).endsWith(QLatin1Char(':'))) {
            qCWarning(UTIL) << "Path::generatePathOrUrl: Invalid Windows drive encountered (expected C: or similar), got: " <<
                qPrintable(data.at(0));
        }
        Q_ASSERT(data.at(0).endsWith(QLatin1Char(':'))); // assume something along "C:"
        res += data.at(0);
        start++;
    }
#endif

    for (int i = start; i < size; ++i) {
        if (i || isLocalFile) {
            res += QLatin1Char('/');
        }

        res += data.at(i);
    }

    return res;
}

QString Path::pathOrUrl() const
{
    return generatePathOrUrl(false, isLocalFile(), m_data);
}

QString Path::path() const
{
    return generatePathOrUrl(true, isLocalFile(), m_data);
}

QString Path::toLocalFile() const
{
    return isLocalFile() ? path() : QString();
}

QString Path::relativePath(const Path& path) const
{
    if (!path.isValid()) {
        return QString();
    }
    if (!isValid() || remotePrefix() != path.remotePrefix()) {
        // different remote destinations or we are invalid, return input as-is
        return path.pathOrUrl();
    }
    // while I'd love to use QUrl::relativePath here, it seems to behave pretty
    // strangely, and adds unexpected "./" at the start for example
    // so instead, do it on our own based on _relativePath in kurl.cpp
    // this should also be more performant I think

    // Find where they meet
    int level = isRemote() ? 1 : 0;
    const int maxLevel = qMin(m_data.count(), path.m_data.count());
    while (level < maxLevel && m_data.at(level) == path.m_data.at(level)) {
        ++level;
    }

    // Need to go down out of our path to the common branch.
    // but keep in mind that e.g. '/' paths have an empty name
    int backwardSegments = m_data.count() - level;
    if (backwardSegments && level < maxLevel && m_data.at(level).isEmpty()) {
        --backwardSegments;
    }

    // Now up from the common branch to the second path.
    int forwardSegmentsLength = 0;
    for (int i = level; i < path.m_data.count(); ++i) {
        forwardSegmentsLength += path.m_data.at(i).length();
        // slashes
        if (i + 1 != path.m_data.count()) {
            forwardSegmentsLength += 1;
        }
    }

    QString relativePath;
    relativePath.reserve((backwardSegments * 3) + forwardSegmentsLength);
    for (int i = 0; i < backwardSegments; ++i) {
        relativePath.append(QLatin1String("../"));
    }

    for (int i = level; i < path.m_data.count(); ++i) {
        relativePath.append(path.m_data.at(i));
        if (i + 1 != path.m_data.count()) {
            relativePath.append(QLatin1Char('/'));
        }
    }

    Q_ASSERT(relativePath.length() == ((backwardSegments * 3) + forwardSegmentsLength));

    return relativePath;
}

static bool isParentPath(const QVector<QString>& parent, const QVector<QString>& child, bool direct)
{
    if (direct && child.size() != parent.size() + 1) {
        return false;
    } else if (!direct && child.size() <= parent.size()) {
        return false;
    }
    for (int i = 0; i < parent.size(); ++i) {
        if (child.at(i) != parent.at(i)) {
            // support for trailing '/'
            if (i + 1 == parent.size() && parent.at(i).isEmpty()) {
                return true;
            }
            // otherwise we take a different branch here
            return false;
        }
    }

    return true;
}

bool Path::isParentOf(const Path& path) const
{
    if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) {
        return false;
    }
    return isParentPath(m_data, path.m_data, false);
}

bool Path::isDirectParentOf(const Path& path) const
{
    if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) {
        return false;
    }
    return isParentPath(m_data, path.m_data, true);
}

QString Path::remotePrefix() const
{
    return isRemote() ? m_data.first() : QString();
}

bool Path::operator<(const Path& other) const
{
    const int size = m_data.size();
    const int otherSize = other.m_data.size();
    const int toCompare = qMin(size, otherSize);

    // compare each Path segment in turn and try to return early
    for (int i = 0; i < toCompare; ++i) {
        int comparison = m_data.at(i).compare(other.m_data.at(i));
        if (comparison == 0) {
            // equal, try next segment
            continue;
        } else {
            // return whether our segment is less then the other one
            return comparison < 0;
        }
    }

    // when we reach this point, all elements that we compared where equal
    // thus return whether we have less items than the other Path
    return size < otherSize;
}

QUrl Path::toUrl() const
{
    return QUrl::fromUserInput(pathOrUrl());
}

bool Path::isLocalFile() const
{
    // if the first data element contains a '/' it is a Path prefix
    return !m_data.isEmpty() && !m_data.first().contains(QLatin1Char('/'));
}

bool Path::isRemote() const
{
    return !m_data.isEmpty() && m_data.first().contains(QLatin1Char('/'));
}

QString Path::lastPathSegment() const
{
    // remote Paths are offset by one, thus never return the first item of them as file name
    if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) {
        return QString();
    }
    return m_data.last();
}

void Path::setLastPathSegment(const QString& name)
{
    // remote Paths are offset by one, thus never return the first item of them as file name
    if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) {
        // append the name to empty Paths or remote Paths only containing the Path prefix
        m_data.append(name);
    } else {
        // overwrite the last data member
        m_data.last() = name;
    }
}

static void cleanPath(QVector<QString>* data, const bool isRemote)
{
    if (data->isEmpty()) {
        return;
    }
    const int startOffset = isRemote ? 1 : 0;
    const auto start = data->begin() + startOffset;

    auto it = start;
    while (it != data->end()) {
        if (*it == QLatin1String("..")) {
            if (it == start) {
                it = data->erase(it);
            } else {
                if (isWindowsDriveLetter(*(it - 1))) {
                    it = data->erase(it); // keep the drive letter
                } else {
                    it = data->erase(it - 1, it + 1);
                }
            }
        } else if (*it == QLatin1String(".")) {
            it = data->erase(it);
        } else {
            ++it;
        }
    }
    if (data->count() == startOffset) {
        data->append(QString());
    }
}

// Optimized QString::split code for the specific Path use-case
static QVarLengthArray<QString, 16> splitPath(const QString& source)
{
    QVarLengthArray<QString, 16> list;
    int start = 0;
    int end = 0;
    while ((end = source.indexOf(QLatin1Char('/'), start)) != -1) {
        if (start != end) {
            list.append(source.mid(start, end - start));
        }
        start = end + 1;
    }
    if (start != source.size()) {
        list.append(source.mid(start, -1));
    }
    return list;
}

void Path::addPath(const QString& path)
{
    if (path.isEmpty()) {
        return;
    }

    const auto& newData = splitPath(path);
    if (newData.isEmpty()) {
        if (m_data.size() == (isRemote() ? 1 : 0)) {
            // this represents the root path, we just turned an invalid path into it
            m_data << QString();
        }
        return;
    }

    auto it = newData.begin();
    if (!m_data.isEmpty() && m_data.last().isEmpty()) {
        // the root item is empty, set its contents and continue appending
        m_data.last() = *it;
        ++it;
    }

    std::copy(it, newData.end(), std::back_inserter(m_data));
    cleanPath(&m_data, isRemote());
}

Path Path::parent() const
{
    if (m_data.isEmpty()) {
        return Path();
    }

    Path ret(*this);
    if (m_data.size() == (1 + (isRemote() ? 1 : 0))) {
        // keep the root item, but clear it, otherwise we'd make the path invalid
        // or a URL a local path
        auto& root = ret.m_data.last();
        if (!isWindowsDriveLetter(root)) {
            root.clear();
        }
    } else {
        ret.m_data.pop_back();
    }
    return ret;
}

bool Path::hasParent() const
{
    const int rootIdx = isRemote() ? 1 : 0;
    return m_data.size() > rootIdx && !m_data[rootIdx].isEmpty();
}

void Path::clear()
{
    m_data.clear();
}

Path Path::cd(const QString& dir) const
{
    if (!isValid()) {
        return Path();
    }
    return Path(*this, dir);
}

namespace KDevelop {
uint qHash(const Path& path)
{
    KDevHash hash;
    const auto pathSegments = path.segments();
    for (const QString& segment : pathSegments) {
        hash << qHash(segment);
    }

    return hash;
}

template<typename Container>
static Path::List toPathList_impl(const Container& list)
{
    Path::List ret;
    ret.reserve(list.size());
    for (const auto& entry : list) {
        Path path(entry);
        if (path.isValid()) {
            ret << path;
        }
    }

    ret.squeeze();
    return ret;
}

Path::List toPathList(const QList<QUrl>& list)
{
    return toPathList_impl(list);
}

Path::List toPathList(const QList<QString>& list)
{
    return toPathList_impl(list);
}

}

QDebug operator<<(QDebug s, const Path& string)
{
    s.nospace() << string.pathOrUrl();
    return s.space();
}

namespace QTest {
template<>
char* toString(const Path& path)
{
    return qstrdup(qPrintable(path.pathOrUrl()));
}
}