// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include #include "../../../../shared/filesystem.h" using namespace Qt::StringLiterals; class tst_QAbstractFileEngine : public QObject { Q_OBJECT public slots: void initTestCase(); void cleanupTestCase(); private slots: void customHandler(); void fileIO_data(); void fileIO(); void mounting_data(); void mounting(); private: QStringList filesForRemoval; QSharedPointer m_currentDir; QString m_previousCurrent; }; class ReferenceFileEngine : public QAbstractFileEngine { public: ReferenceFileEngine(const QString &fileName) : fileName_(QDir::cleanPath(fileName)) , position_(-1) , openForRead_(false) , openForWrite_(false) { } bool open(QIODevice::OpenMode openMode, std::optional permissions) override { Q_UNUSED(permissions); if (openForRead_ || openForWrite_) { qWarning("%s: file is already open for %s", Q_FUNC_INFO, (openForRead_ ? "reading" : "writing")); return false; } openFile_ = resolveFile(openMode & QIODevice::WriteOnly); if (!openFile_) return false; position_ = 0; if (openMode & QIODevice::ReadOnly) openForRead_ = true; if (openMode & QIODevice::WriteOnly) { openForWrite_ = true; QMutexLocker lock(&openFile_->mutex); if (openMode & QIODevice::Truncate || !(openForRead_ || openMode & QIODevice::Append)) openFile_->content.clear(); if (openMode & QIODevice::Append) position_ = openFile_->content.size(); } return true; } bool close() override { openFile_.clear(); openForRead_ = false; openForWrite_ = false; position_ = -1; return true; } qint64 size() const override { QSharedPointer file = resolveFile(false); if (!file) return 0; QMutexLocker lock(&file->mutex); return file->content.size(); } qint64 pos() const override { if (!openForRead_ && !openForWrite_) { qWarning("%s: file is not open", Q_FUNC_INFO); return -1; } return position_; } bool seek(qint64 pos) override { if (!openForRead_ && !openForWrite_) { qWarning("%s: file is not open", Q_FUNC_INFO); return false; } if (pos >= 0) { position_ = pos; return true; } return false; } bool flush() override { if (!openForRead_ && !openForWrite_) { qWarning("%s: file is not open", Q_FUNC_INFO); return false; } return true; } bool remove() override { QMutexLocker lock(&fileSystemMutex); int count = fileSystem.remove(fileName_); return (count == 1); } bool copy(const QString &newName) override { QMutexLocker lock(&fileSystemMutex); if (!fileSystem.contains(fileName_) || fileSystem.contains(newName)) return false; fileSystem.insert(newName, fileSystem.value(fileName_)); return true; } bool rename(const QString &newName) override { QMutexLocker lock(&fileSystemMutex); if (!fileSystem.contains(fileName_) || fileSystem.contains(newName)) return false; fileSystem.insert(newName, fileSystem.take(fileName_)); return true; } bool setSize(qint64 size) override { if (size < 0) return false; QSharedPointer file = resolveFile(false); if (!file) return false; QMutexLocker lock(&file->mutex); file->content.resize(size); if (openForRead_ || openForWrite_) if (position_ > size) position_ = size; return (file->content.size() == size); } FileFlags fileFlags(FileFlags type) const override { QSharedPointer file = resolveFile(false); if (file) { QMutexLocker lock(&file->mutex); return (file->fileFlags & type); } return FileFlags(); } QString fileName(FileName file) const override { switch (file) { case DefaultName: return QLatin1String("DefaultName"); case BaseName: return QLatin1String("BaseName"); case PathName: return QLatin1String("PathName"); case AbsoluteName: return QLatin1String("AbsoluteName"); case AbsolutePathName: return QLatin1String("AbsolutePathName"); case AbsoluteLinkTarget: return QLatin1String("AbsoluteLinkTarget"); case RawLinkPath: return QLatin1String("RawLinkPath"); case CanonicalName: return QLatin1String("CanonicalName"); case CanonicalPathName: return QLatin1String("CanonicalPathName"); case BundleName: return QLatin1String("BundleName"); default: break; } return QString(); } uint ownerId(FileOwner owner) const override { QSharedPointer file = resolveFile(false); if (file) { switch (owner) { case OwnerUser: { QMutexLocker lock(&file->mutex); return file->userId; } case OwnerGroup: { QMutexLocker lock(&file->mutex); return file->groupId; } } } return -2; } QString owner(FileOwner owner) const override { QSharedPointer file = resolveFile(false); if (file) { uint ownerId; switch (owner) { case OwnerUser: { QMutexLocker lock(&file->mutex); ownerId = file->userId; } { QMutexLocker lock(&fileSystemMutex); return fileSystemUsers.value(ownerId); } case OwnerGroup: { QMutexLocker lock(&file->mutex); ownerId = file->groupId; } { QMutexLocker lock(&fileSystemMutex); return fileSystemGroups.value(ownerId); } } } return QString(); } QDateTime fileTime(QFile::FileTime time) const override { QSharedPointer file = resolveFile(false); if (file) { QMutexLocker lock(&file->mutex); switch (time) { case QFile::FileBirthTime: return file->birth; case QFile::FileMetadataChangeTime: return file->change; case QFile::FileModificationTime: return file->modification; case QFile::FileAccessTime: return file->access; } } return QDateTime(); } void setFileName(const QString &file) override { if (openForRead_ || openForWrite_) qWarning("%s: Can't set file name while file is open", Q_FUNC_INFO); else fileName_ = file; } qint64 read(char *data, qint64 maxLen) override { if (!openForRead_) { qWarning("%s: file must be open for reading", Q_FUNC_INFO); return -1; } if (openFile_.isNull()) { qWarning("%s: file must not be null", Q_FUNC_INFO); return -1; } QMutexLocker lock(&openFile_->mutex); qint64 readSize = qMin(openFile_->content.size() - position_, maxLen); if (readSize < 0) return -1; memcpy(data, openFile_->content.constData() + position_, readSize); position_ += readSize; return readSize; } qint64 write(const char *data, qint64 length) override { if (!openForWrite_) { qWarning("%s: file must be open for writing", Q_FUNC_INFO); return -1; } if (openFile_.isNull()) { qWarning("%s: file must not be null", Q_FUNC_INFO); return -1; } if (length < 0) return -1; QMutexLocker lock(&openFile_->mutex); if (openFile_->content.size() == position_) openFile_->content.append(data, length); else { if (position_ + length > openFile_->content.size()) openFile_->content.resize(position_ + length); openFile_->content.replace(position_, length, data, length); } qint64 writeSize = qMin(length, openFile_->content.size() - position_); position_ += writeSize; return writeSize; } protected: // void setError(QFile::FileError error, const QString &str); struct File { File() : userId(0) , groupId(0) , fileFlags( ReadOwnerPerm | WriteOwnerPerm | ExeOwnerPerm | ReadUserPerm | WriteUserPerm | ExeUserPerm | ReadGroupPerm | WriteGroupPerm | ExeGroupPerm | ReadOtherPerm | WriteOtherPerm | ExeOtherPerm | FileType | ExistsFlag) { } QMutex mutex; uint userId, groupId; QAbstractFileEngine::FileFlags fileFlags; QDateTime birth, change, modification, access; QByteArray content; }; QSharedPointer resolveFile(bool create) const { if (openForRead_ || openForWrite_) { if (!openFile_) qWarning("%s: file should not be null", Q_FUNC_INFO); return openFile_; } QMutexLocker lock(&fileSystemMutex); if (create) { QSharedPointer &p = fileSystem[fileName_]; if (p.isNull()) p = QSharedPointer::create(); return p; } return fileSystem.value(fileName_); } static QMutex fileSystemMutex; static QHash fileSystemUsers, fileSystemGroups; static QHash > fileSystem; private: QString fileName_; qint64 position_; bool openForRead_; bool openForWrite_; mutable QSharedPointer openFile_; }; class MountingFileEngine : public QFSFileEngine { public: class Iterator : public QAbstractFileEngineIterator { public: Iterator(const QString &path, QDir::Filters filters, const QStringList &filterNames) : QAbstractFileEngineIterator(path, filters, filterNames) { names.append("foo"); names.append("bar"); index = -1; } QString currentFileName() const override { if (!names.isEmpty() && index < names.size()) return names.at(index); return {}; } bool advance() override { if (names.isEmpty()) return false; if (index < names.size() - 1) { ++index; return true; } return false; } QStringList names; int index; }; MountingFileEngine(QString fileName) : QFSFileEngine(fileName) { } IteratorUniquePtr beginEntryList(const QString &path, QDir::Filters filters, const QStringList &filterNames) override { return std::make_unique(path, filters, filterNames); } FileFlags fileFlags(FileFlags type) const override { if (fileName(DefaultName).endsWith(".tar")) { FileFlags ret = QFSFileEngine::fileFlags(type); //make this file in file system appear to be a directory ret &= ~FileType; ret |= DirectoryType; return ret; } else { //file inside the archive return ExistsFlag | FileType; } } }; QMutex ReferenceFileEngine::fileSystemMutex; QHash ReferenceFileEngine::fileSystemUsers, ReferenceFileEngine::fileSystemGroups; QHash > ReferenceFileEngine::fileSystem; class FileEngineHandler : QAbstractFileEngineHandler { Q_DISABLE_COPY_MOVE(FileEngineHandler) std::unique_ptr create(const QString &fileName) const override { if (fileName.endsWith(".tar") || fileName.contains(".tar/")) return std::make_unique(fileName); if (auto l1 = "QFSFileEngine:"_L1; fileName.startsWith(l1)) return std::make_unique(fileName.sliced(l1.size())); if (auto l1 = "reference-file-engine:"_L1; fileName.startsWith(l1)) return std::make_unique(fileName.sliced(l1.size())); if (auto l1 = "resource:"_L1; fileName.startsWith(l1)) { const auto p = ":/tst_qabstractfileengine/resources/"_L1 + fileName.sliced(l1.size()); return QAbstractFileEngine::create(p); } return nullptr; } public: FileEngineHandler() = default; }; void tst_QAbstractFileEngine::initTestCase() { m_previousCurrent = QDir::currentPath(); m_currentDir = QSharedPointer::create(); QVERIFY2(!m_currentDir.isNull(), qPrintable("Could not create current directory.")); QDir::setCurrent(m_currentDir->path()); } void tst_QAbstractFileEngine::cleanupTestCase() { bool failed = false; FileEngineHandler handler; for (const QString &file : std::as_const(filesForRemoval)) { if (!QFile::remove(file) || QFile::exists(file)) { failed = true; qDebug() << "Couldn't remove file:" << file; } } QVERIFY(!failed); QDir::setCurrent(m_previousCurrent); } void tst_QAbstractFileEngine::customHandler() { std::unique_ptr file; { file = QAbstractFileEngine::create(u"resource:file.txt"_s); QVERIFY(file); } { FileEngineHandler handler; QFile file("resource:file.txt"); QVERIFY(file.exists()); } { QFile file("resource:file.txt"); QVERIFY(!file.exists()); } } void tst_QAbstractFileEngine::fileIO_data() { QTest::addColumn("fileName"); QTest::addColumn("readContent"); QTest::addColumn("writeContent"); QTest::addColumn("fileExists"); QString resourceTxtFile(":/tst_qabstractfileengine/resources/file.txt"); QByteArray readContent("This is a simple text file.\n"); QByteArray writeContent("This contains two lines of text.\n"); QTest::newRow("resource") << resourceTxtFile << readContent << QByteArray() << true; QTest::newRow("native") << "native-file.txt" << readContent << writeContent << false; QTest::newRow("Forced QFSFileEngine") << "QFSFileEngine:QFSFileEngine-file.txt" << readContent << writeContent << false; QTest::newRow("Custom FE") << "reference-file-engine:file.txt" << readContent << writeContent << false; QTest::newRow("Forced QFSFileEngine (native)") << "QFSFileEngine:native-file.txt" << readContent << writeContent << true; QTest::newRow("native (Forced QFSFileEngine)") << "QFSFileEngine-file.txt" << readContent << writeContent << true; QTest::newRow("Custom FE (2)") << "reference-file-engine:file.txt" << readContent << writeContent << true; } void tst_QAbstractFileEngine::fileIO() { QFETCH(QString, fileName); QFETCH(QByteArray, readContent); QFETCH(QByteArray, writeContent); QFETCH(bool, fileExists); FileEngineHandler handler; { QFile file(fileName); QCOMPARE(file.exists(), fileExists); if (!fileExists) { QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Unbuffered)); filesForRemoval.append(fileName); QCOMPARE(file.write(readContent), qint64(readContent.size())); } } // // File content is: readContent // qint64 fileSize = readContent.size(); { // Reading QFile file(fileName); QVERIFY(!file.isOpen()); /* For an exact match, this test requires the repository to * be checked out with UNIX-style line endings on Windows. * Try to succeed also for the common case of checking out with autocrlf * by reading the file as text and checking if the size matches * the original size + the '\r' characters added by autocrlf. */ QFile::OpenMode openMode = QIODevice::ReadOnly | QIODevice::Unbuffered; #if defined (Q_OS_WIN) || defined(Q_OS_WASM) openMode |= QIODevice::Text; #endif QVERIFY(file.open(openMode)); QVERIFY(file.isOpen()); #if defined(Q_OS_WIN) || defined(Q_OS_WASM) const qint64 convertedSize = fileSize + readContent.count('\n'); if (file.size() == convertedSize) fileSize = convertedSize; #endif QCOMPARE(file.size(), fileSize); QCOMPARE(file.pos(), qint64(0)); QCOMPARE(file.size(), fileSize); QCOMPARE(file.readAll(), readContent); QCOMPARE(file.pos(), fileSize); file.close(); QVERIFY(!file.isOpen()); QCOMPARE(file.size(), fileSize); } if (writeContent.isEmpty()) return; { // Writing / appending QFile file(fileName); QVERIFY(!file.isOpen()); QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Unbuffered)); QVERIFY(file.isOpen()); QCOMPARE(file.size(), fileSize); QCOMPARE(file.pos(), fileSize); QCOMPARE(file.write(writeContent), qint64(writeContent.size())); fileSize += writeContent.size(); QCOMPARE(file.pos(), fileSize); QCOMPARE(file.size(), fileSize); file.close(); QVERIFY(!file.isOpen()); QCOMPARE(file.size(), fileSize); } // // File content is: readContent + writeContent // { // Reading and Writing QFile file(fileName); QVERIFY(!file.isOpen()); QVERIFY(file.open(QIODevice::ReadWrite | QIODevice::Unbuffered)); QVERIFY(file.isOpen()); QCOMPARE(file.size(), fileSize); QCOMPARE(file.pos(), qint64(0)); QCOMPARE(file.readAll(), readContent + writeContent); QCOMPARE(file.pos(), fileSize); QCOMPARE(file.size(), fileSize); QVERIFY(file.seek(writeContent.size())); QCOMPARE(file.pos(), qint64(writeContent.size())); QCOMPARE(file.size(), fileSize); QCOMPARE(file.write(readContent), qint64(readContent.size())); QCOMPARE(file.pos(), fileSize); QCOMPARE(file.size(), fileSize); QVERIFY(file.seek(0)); QCOMPARE(file.pos(), qint64(0)); QCOMPARE(file.size(), fileSize); QCOMPARE(file.write(writeContent), qint64(writeContent.size())); QCOMPARE(file.pos(), qint64(writeContent.size())); QCOMPARE(file.size(), fileSize); QVERIFY(file.seek(0)); QCOMPARE(file.read(writeContent.size()), writeContent); QCOMPARE(file.pos(), qint64(writeContent.size())); QCOMPARE(file.size(), fileSize); QCOMPARE(file.readAll(), readContent); QCOMPARE(file.pos(), fileSize); QCOMPARE(file.size(), fileSize); file.close(); QVERIFY(!file.isOpen()); QCOMPARE(file.size(), fileSize); } // // File content is: writeContent + readContent // { // Writing QFile file(fileName); QVERIFY(!file.isOpen()); QVERIFY(file.open(QIODevice::ReadWrite | QIODevice::Unbuffered)); QVERIFY(file.isOpen()); QCOMPARE(file.size(), fileSize); QCOMPARE(file.pos(), qint64(0)); QCOMPARE(file.write(writeContent), qint64(writeContent.size())); QCOMPARE(file.pos(), qint64(writeContent.size())); QCOMPARE(file.size(), fileSize); QVERIFY(file.resize(writeContent.size())); QCOMPARE(file.size(), qint64(writeContent.size())); file.close(); QVERIFY(!file.isOpen()); QCOMPARE(file.size(), qint64(writeContent.size())); QVERIFY(file.resize(fileSize)); QCOMPARE(file.size(), fileSize); } // // File content is: writeContent + // File size is : (readContent + writeContent).size() // { // Writing / extending QFile file(fileName); QVERIFY(!file.isOpen()); QVERIFY(file.open(QIODevice::ReadWrite | QIODevice::Unbuffered)); QVERIFY(file.isOpen()); QCOMPARE(file.size(), fileSize); QCOMPARE(file.pos(), qint64(0)); QVERIFY(file.seek(1024)); QCOMPARE(file.pos(), qint64(1024)); QCOMPARE(file.size(), fileSize); fileSize = 1024 + writeContent.size(); QCOMPARE(file.write(writeContent), qint64(writeContent.size())); QCOMPARE(file.pos(), fileSize); QCOMPARE(file.size(), fileSize); QVERIFY(file.seek(1028)); QCOMPARE(file.pos(), qint64(1028)); QCOMPARE(file.size(), fileSize); fileSize = 1028 + writeContent.size(); QCOMPARE(file.write(writeContent), qint64(writeContent.size())); QCOMPARE(file.pos(), fileSize); QCOMPARE(file.size(), fileSize); file.close(); QVERIFY(!file.isOpen()); QCOMPARE(file.size(), fileSize); } // // File content is: writeContent + + writeContent // File size is : 1024 + writeContent.size() // { // Writing / truncating QFile file(fileName); QVERIFY(!file.isOpen()); QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Unbuffered)); QVERIFY(file.isOpen()); QCOMPARE(file.size(), qint64(0)); QCOMPARE(file.pos(), qint64(0)); fileSize = readContent.size(); QCOMPARE(file.write(readContent), fileSize); QCOMPARE(file.pos(), fileSize); QCOMPARE(file.size(), fileSize); file.close(); QVERIFY(!file.isOpen()); QCOMPARE(file.size(), fileSize); } // // File content is: readContent // } void tst_QAbstractFileEngine::mounting_data() { QTest::addColumn("fileName"); QTest::newRow("native") << "test.tar"; QTest::newRow("Forced QFSFileEngine") << "QFSFileEngine:test.tar"; } void tst_QAbstractFileEngine::mounting() { FileSystem fs; QVERIFY(fs.createFile("test.tar")); FileEngineHandler handler; QFETCH(QString, fileName); const QString absName = fs.absoluteFilePath(fileName); QVERIFY(QFileInfo(absName).isDir()); QDir dir(absName); QCOMPARE(dir.entryList(), (QStringList() << "bar" << "foo")); QDir dir2(fs.path()); bool found = false; const auto entries = dir2.entryInfoList(); for (const QFileInfo &info : entries) { if (info.fileName() == QLatin1String("test.tar")) { QVERIFY(!found); found = true; QVERIFY(info.isDir()); } } QVERIFY(found); } QTEST_APPLESS_MAIN(tst_QAbstractFileEngine) #include "tst_qabstractfileengine.moc"