Extend Utils::FilePath for relative paths

The new methods allows converting a path to a file or directory into a
path relative to another path to a file or directory.

Change-Id: I8c743d5bced9fec81b05ce94ac2b7bec307d9028
Reviewed-by: André Hartmann <aha_1980@gmx.de>
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Jochen Becher
2021-04-06 10:46:25 +02:00
parent 872660fcc5
commit 05324cf21f
4 changed files with 199 additions and 0 deletions

View File

@@ -44,6 +44,7 @@ private:
public:
static QString convertFileNameToElementName(const QString &fileName);
static QString convertElementNameToBaseFileName(const QString &elementName);
// TODO use Utils::FilePath instead
static QString calcRelativePath(const QString &absoluteFileName, const QString &anchorPath);
static QString calcElementNameSearchId(const QString &elementName);
static QStringList buildElementsPath(const QString &filePath, bool ignoreLastFilePathPart);

View File

@@ -821,6 +821,15 @@ FilePath FilePath::absolutePath() const
return result;
}
/// Constructs an absolute FilePath from this path which
/// is interpreted as being relative to \a anchor.
FilePath FilePath::absoluteFromRelativePath(const FilePath &anchor) const
{
QDir anchorDir = QFileInfo(anchor.m_data).absoluteDir();
QString absoluteFilePath = QFileInfo(anchorDir, m_data).canonicalFilePath();
return FilePath::fromString(absoluteFilePath);
}
/// Constructs a FilePath from \a filename
/// \a filename is not checked for validity.
FilePath FilePath::fromString(const QString &filename)
@@ -970,6 +979,94 @@ FilePath FilePath::relativeChildPath(const FilePath &parent) const
return FilePath::fromString(m_data.mid(parent.m_data.size() + 1, -1));
}
/// \returns the relativePath of FilePath to given \a anchor.
/// Both, FilePath and anchor may be files or directories.
/// Example usage:
///
/// \code
/// FilePath filePath("/foo/b/ar/file.txt");
/// FilePath relativePath = filePath.relativePath("/foo/c");
/// qDebug() << relativePath
/// \endcode
///
/// The debug output will be "../b/ar/file.txt".
///
FilePath FilePath::relativePath(const FilePath &anchor) const
{
const QFileInfo fileInfo(m_data);
QString absolutePath;
QString filename;
if (fileInfo.isFile()) {
absolutePath = fileInfo.absolutePath();
filename = fileInfo.fileName();
} else if (fileInfo.isDir()) {
absolutePath = fileInfo.absoluteFilePath();
} else {
return {};
}
const QFileInfo anchorInfo(anchor.m_data);
QString absoluteAnchorPath;
if (anchorInfo.isFile())
absoluteAnchorPath = anchorInfo.absolutePath();
else if (anchorInfo.isDir())
absoluteAnchorPath = anchorInfo.absoluteFilePath();
else
return {};
QString relativeFilePath = calcRelativePath(absolutePath, absoluteAnchorPath);
if (!filename.isEmpty()) {
if (!relativeFilePath.isEmpty())
relativeFilePath += '/';
relativeFilePath += filename;
}
return FilePath::fromString(relativeFilePath);
}
/// \returns the relativePath of \a absolutePath to given \a absoluteAnchorPath.
/// Both paths must be an absolute path to a directory. Example usage:
///
/// \code
/// qDebug() << FilePath::calcRelativePath("/foo/b/ar", "/foo/c");
/// \endcode
///
/// The debug output will be "../b/ar".
///
/// \see FilePath::relativePath
///
QString FilePath::calcRelativePath(const QString &absolutePath, const QString &absoluteAnchorPath)
{
if (absolutePath.isEmpty() || absoluteAnchorPath.isEmpty())
return QString();
// TODO using split() instead of parsing the strings by char index is slow
// and needs more memory (but the easiest implementation for now)
const QStringList splits1 = absolutePath.split('/');
const QStringList splits2 = absoluteAnchorPath.split('/');
int i = 0;
while (i < splits1.count() && i < splits2.count() && splits1.at(i) == splits2.at(i))
++i;
QString relativePath;
int j = i;
bool addslash = false;
while (j < splits2.count()) {
if (!splits2.at(j).isEmpty()) {
if (addslash)
relativePath += '/';
relativePath += "..";
addslash = true;
}
++j;
}
while (i < splits1.count()) {
if (!splits1.at(i).isEmpty()) {
if (addslash)
relativePath += '/';
relativePath += splits1.at(i);
addslash = true;
}
++i;
}
return relativePath;
}
FilePath FilePath::pathAppended(const QString &str) const
{
FilePath fn = *this;

View File

@@ -61,6 +61,9 @@ extern Q_CORE_EXPORT int qt_ntfs_permission_lookup;
QT_END_NAMESPACE
// tst_fileutils becomes a friend of Utils::FilePath for testing private method
class tst_fileutils;
namespace Utils {
class QTCREATOR_UTILS_EXPORT FilePath
@@ -89,6 +92,7 @@ public:
FilePath parentDir() const;
FilePath absolutePath() const;
FilePath absoluteFromRelativePath(const FilePath &anchor) const;
bool operator==(const FilePath &other) const;
bool operator!=(const FilePath &other) const;
@@ -107,6 +111,7 @@ public:
bool isNewerThan(const QDateTime &timeStamp) const;
FilePath relativeChildPath(const FilePath &parent) const;
FilePath relativePath(const FilePath &anchor) const;
FilePath pathAppended(const QString &str) const;
FilePath stringAppended(const QString &str) const;
FilePath resolvePath(const QString &fileName) const;
@@ -126,6 +131,9 @@ public:
QUrl toUrl() const;
private:
friend class ::tst_fileutils;
static QString calcRelativePath(const QString &absolutePath, const QString &absoluteAnchorPath);
QString m_data;
QUrl m_url;
};

View File

@@ -38,14 +38,47 @@ class tst_fileutils : public QObject
public:
private slots:
void initTestCase();
void parentDir_data();
void parentDir();
void isChildOf_data();
void isChildOf();
void fileName_data();
void fileName();
void calcRelativePath_data();
void calcRelativePath();
void relativePath_specials();
void relativePath_data();
void relativePath();
private:
QTemporaryDir tempDir;
QString rootPath;
};
static void touch(const QDir &dir, const QString &filename)
{
QFile file(dir.absoluteFilePath(filename));
file.open(QIODevice::WriteOnly);
file.close();
}
void tst_fileutils::initTestCase()
{
// initialize test for tst_fileutiles::relativePath*()
QVERIFY(tempDir.isValid());
rootPath = tempDir.path();
QDir dir(rootPath);
dir.mkpath("a/b/c/d");
dir.mkpath("a/x/y/z");
dir.mkpath("a/b/x/y/z");
dir.mkpath("x/y/z");
touch(dir, "a/b/c/d/file1.txt");
touch(dir, "a/x/y/z/file2.txt");
touch(dir, "a/file3.txt");
touch(dir, "x/y/file4.txt");
}
void tst_fileutils::parentDir_data()
{
QTest::addColumn<QString>("path");
@@ -164,5 +197,65 @@ void tst_fileutils::fileName()
QCOMPARE(FilePath::fromString(path).fileNameWithPathComponents(components), result);
}
void tst_fileutils::calcRelativePath_data()
{
QTest::addColumn<QString>("absolutePath");
QTest::addColumn<QString>("anchorPath");
QTest::addColumn<QString>("result");
QTest::newRow("empty") << "" << "" << "";
QTest::newRow("leftempty") << "" << "/" << "";
QTest::newRow("rightempty") << "/" << "" << "";
QTest::newRow("root") << "/" << "/" << "";
QTest::newRow("simple1") << "/a" << "/" << "a";
QTest::newRow("simple2") << "/" << "/a" << "..";
QTest::newRow("simple3") << "/a" << "/a" << "";
QTest::newRow("extraslash1") << "/a/b/c" << "/a/b/c" << "";
QTest::newRow("extraslash2") << "/a/b/c" << "/a/b/c/" << "";
QTest::newRow("extraslash3") << "/a/b/c/" << "/a/b/c" << "";
QTest::newRow("normal1") << "/a/b/c" << "/a/x" << "../b/c";
QTest::newRow("normal2") << "/a/b/c" << "/a/x/y" << "../../b/c";
QTest::newRow("normal3") << "/a/b/c" << "/x/y" << "../../a/b/c";
}
void tst_fileutils::calcRelativePath()
{
QFETCH(QString, absolutePath);
QFETCH(QString, anchorPath);
QFETCH(QString, result);
QString relativePath = Utils::FilePath::calcRelativePath(absolutePath, anchorPath);
QCOMPARE(relativePath, result);
}
void tst_fileutils::relativePath_specials()
{
QString path = FilePath::fromString("").relativePath(FilePath::fromString("")).toString();
QCOMPARE(path, "");
}
void tst_fileutils::relativePath_data()
{
QTest::addColumn<QString>("relative");
QTest::addColumn<QString>("anchor");
QTest::addColumn<QString>("result");
QTest::newRow("samedir") << "/" << "/" << "";
QTest::newRow("dir2dir_1") << "a/b/c/d" << "a/x/y/z" << "../../../b/c/d";
QTest::newRow("dir2dir_2") << "a/b" <<"a/b/c" << "..";
QTest::newRow("file2file_1") << "a/b/c/d/file1.txt" << "a/file3.txt" << "b/c/d/file1.txt";
QTest::newRow("dir2file_1") << "a/b/c" << "a/x/y/z/file2.txt" << "../../../b/c";
QTest::newRow("file2dir_1") << "a/b/c/d/file1.txt" << "x/y" << "../../a/b/c/d/file1.txt";
}
void tst_fileutils::relativePath()
{
QFETCH(QString, relative);
QFETCH(QString, anchor);
QFETCH(QString, result);
FilePath actualPath = FilePath::fromString(rootPath + "/" + relative)
.relativePath(FilePath::fromString(rootPath + "/" + anchor));
QCOMPARE(actualPath.toString(), result);
}
QTEST_APPLESS_MAIN(tst_fileutils)
#include "tst_fileutils.moc"