/* * This software is in the public domain, furnished "as is", without technical * support, and with no warranty, express or implied, as to its usefulness for * any purpose. * */ #include "syncenginetestutils.h" #include "httplogger.h" #include "accessmanager.h" #include "gui/sharepermissions.h" #include #include #include #include #include PathComponents::PathComponents(const char *path) : PathComponents { QString::fromUtf8(path) } { } PathComponents::PathComponents(const QString &path) : QStringList { path.split(QLatin1Char('/'), Qt::SkipEmptyParts) } { } PathComponents::PathComponents(const QStringList &pathComponents) : QStringList { pathComponents } { } PathComponents PathComponents::parentDirComponents() const { return PathComponents { mid(0, size() - 1) }; } PathComponents PathComponents::subComponents() const & { return PathComponents { mid(1) }; } void DiskFileModifier::remove(const QString &relativePath) { QFileInfo fi { _rootDir.filePath(relativePath) }; if (fi.isFile()) QVERIFY(_rootDir.remove(relativePath)); else QVERIFY(QDir { fi.filePath() }.removeRecursively()); } void DiskFileModifier::insert(const QString &relativePath, qint64 size, char contentChar) { QFile file { _rootDir.filePath(relativePath) }; QVERIFY(!file.exists()); file.open(QFile::WriteOnly); QByteArray buf(1024, contentChar); for (int x = 0; x < size / buf.size(); ++x) { file.write(buf); } file.write(buf.data(), size % buf.size()); file.close(); // Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs. OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc().addSecs(-30))); QCOMPARE(file.size(), size); } void DiskFileModifier::setContents(const QString &relativePath, char contentChar) { QFile file { _rootDir.filePath(relativePath) }; QVERIFY(file.exists()); qint64 size = file.size(); file.open(QFile::WriteOnly); file.write(QByteArray {}.fill(contentChar, size)); } void DiskFileModifier::appendByte(const QString &relativePath) { QFile file { _rootDir.filePath(relativePath) }; QVERIFY(file.exists()); file.open(QFile::ReadWrite); QByteArray contents = file.read(1); file.seek(file.size()); file.write(contents); } void DiskFileModifier::mkdir(const QString &relativePath) { _rootDir.mkpath(relativePath); } void DiskFileModifier::rename(const QString &from, const QString &to) { QVERIFY(_rootDir.exists(from)); QVERIFY(_rootDir.rename(from, to)); } void DiskFileModifier::setModTime(const QString &relativePath, const QDateTime &modTime) { OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime)); } void DiskFileModifier::modifyLockState([[maybe_unused]] const QString &relativePath, [[maybe_unused]] LockState lockState, [[maybe_unused]] int lockType, [[maybe_unused]] const QString &lockOwner, [[maybe_unused]] const QString &lockOwnerId, [[maybe_unused]] const QString &lockEditorId, [[maybe_unused]] quint64 lockTime, [[maybe_unused]] quint64 lockTimeout) { } void DiskFileModifier::setE2EE([[maybe_unused]] const QString &relativePath, [[maybe_unused]] const bool enable) { } FileInfo FileInfo::A12_B12_C12_S12() { FileInfo fi { QString {}, { { QStringLiteral("A"), { { QStringLiteral("a1"), 4 }, { QStringLiteral("a2"), 4 } } }, { QStringLiteral("B"), { { QStringLiteral("b1"), 16 }, { QStringLiteral("b2"), 16 } } }, { QStringLiteral("C"), { { QStringLiteral("c1"), 24 }, { QStringLiteral("c2"), 24 } } }, } }; FileInfo sharedFolder { QStringLiteral("S"), { { QStringLiteral("s1"), 32 }, { QStringLiteral("s2"), 32 } } }; sharedFolder.isShared = true; sharedFolder.children[QStringLiteral("s1")].isShared = true; sharedFolder.children[QStringLiteral("s2")].isShared = true; fi.children.insert(sharedFolder.name, std::move(sharedFolder)); return fi; } FileInfo::FileInfo(const QString &name, const std::initializer_list &children) : name { name } { for (const auto &source : children) addChild(source); } void FileInfo::addChild(const FileInfo &info) { auto &dest = this->children[info.name] = info; dest.parentPath = path(); dest.fixupParentPathRecursively(); } void FileInfo::remove(const QString &relativePath) { const PathComponents pathComponents { relativePath }; FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); Q_ASSERT(parent); parent->children.erase(std::find_if(parent->children.begin(), parent->children.end(), [&pathComponents](const FileInfo &fi) { return fi.name == pathComponents.fileName(); })); } void FileInfo::insert(const QString &relativePath, qint64 size, char contentChar) { create(relativePath, size, contentChar); } void FileInfo::setContents(const QString &relativePath, char contentChar) { FileInfo *file = findInvalidatingEtags(relativePath); Q_ASSERT(file); file->contentChar = contentChar; } void FileInfo::appendByte(const QString &relativePath) { FileInfo *file = findInvalidatingEtags(relativePath); Q_ASSERT(file); file->size += 1; } void FileInfo::mkdir(const QString &relativePath) { createDir(relativePath); } void FileInfo::rename(const QString &oldPath, const QString &newPath) { const PathComponents newPathComponents { newPath }; FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents()); Q_ASSERT(dir); Q_ASSERT(dir->isDir); const PathComponents pathComponents { oldPath }; FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); Q_ASSERT(parent); FileInfo fi = parent->children.take(pathComponents.fileName()); fi.parentPath = dir->path(); fi.name = newPathComponents.fileName(); fi.fixupParentPathRecursively(); dir->children.insert(newPathComponents.fileName(), std::move(fi)); } void FileInfo::setModTime(const QString &relativePath, const QDateTime &modTime) { FileInfo *file = findInvalidatingEtags(relativePath); Q_ASSERT(file); file->lastModified = modTime; } void FileInfo::setModTimeKeepEtag(const QString &relativePath, const QDateTime &modTime) { FileInfo *file = find(relativePath); Q_ASSERT(file); file->lastModified = modTime; } void FileInfo::modifyLockState(const QString &relativePath, LockState lockState, int lockType, const QString &lockOwner, const QString &lockOwnerId, const QString &lockEditorId, quint64 lockTime, quint64 lockTimeout) { FileInfo *file = findInvalidatingEtags(relativePath); Q_ASSERT(file); file->lockState = lockState; file->lockType = lockType; file->lockOwner = lockOwner; file->lockOwnerId = lockOwnerId; file->lockEditorId = lockEditorId; file->lockTime = lockTime; file->lockTimeout = lockTimeout; } void FileInfo::setE2EE(const QString &relativePath, const bool enable) { FileInfo *file = findInvalidatingEtags(relativePath); Q_ASSERT(file); file->isEncrypted = enable; } FileInfo *FileInfo::find(PathComponents pathComponents, const bool invalidateEtags) { if (pathComponents.isEmpty()) { if (invalidateEtags) { etag = generateEtag(); } return this; } QString childName = pathComponents.pathRoot(); auto it = children.find(childName); if (it != children.end()) { auto file = it->find(std::move(pathComponents).subComponents(), invalidateEtags); if (file && invalidateEtags) { // Update parents on the way back etag = generateEtag(); } return file; } return nullptr; } FileInfo *FileInfo::createDir(const QString &relativePath) { const PathComponents pathComponents { relativePath }; FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); Q_ASSERT(parent); FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo { pathComponents.fileName() }; child.parentPath = parent->path(); child.etag = generateEtag(); return &child; } FileInfo *FileInfo::create(const QString &relativePath, qint64 size, char contentChar) { const PathComponents pathComponents { relativePath }; FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents()); Q_ASSERT(parent); FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo { pathComponents.fileName(), size }; child.parentPath = parent->path(); child.contentChar = contentChar; child.etag = generateEtag(); return &child; } bool FileInfo::operator==(const FileInfo &other) const { // Consider files to be equal between local<->remote as a user would. return name == other.name && isDir == other.isDir && size == other.size && contentChar == other.contentChar && children == other.children; } QString FileInfo::path() const { return (parentPath.isEmpty() ? QString() : (parentPath + QLatin1Char('/'))) + name; } QString FileInfo::absolutePath() const { if (parentPath.endsWith(QLatin1Char('/'))) { return parentPath + name; } else { return parentPath + QLatin1Char('/') + name; } } void FileInfo::fixupParentPathRecursively() { auto p = path(); for (auto it = children.begin(); it != children.end(); ++it) { Q_ASSERT(it.key() == it->name); it->parentPath = p; it->fixupParentPathRecursively(); } } FileInfo *FileInfo::findInvalidatingEtags(PathComponents pathComponents) { return find(std::move(pathComponents), true); } FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isNull()); // for root, it should be empty const FileInfo *fileInfo = remoteRootFileInfo.find(fileName); if (!fileInfo) { QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection); return; } const QString prefix = request.url().path().left(request.url().path().size() - fileName.size()); // Don't care about the request and just return a full propfind const QString davUri { QStringLiteral("DAV:") }; const QString ocUri { QStringLiteral("http://owncloud.org/ns") }; const QString ncUri { QStringLiteral("http://nextcloud.org/ns") }; QBuffer buffer { &payload }; buffer.open(QIODevice::WriteOnly); QXmlStreamWriter xml(&buffer); xml.writeNamespace(davUri, QStringLiteral("d")); xml.writeNamespace(ocUri, QStringLiteral("oc")); xml.writeNamespace(ncUri, QStringLiteral("nc")); xml.writeStartDocument(); xml.writeStartElement(davUri, QStringLiteral("multistatus")); auto writeFileResponse = [&](const FileInfo &fileInfo) { xml.writeStartElement(davUri, QStringLiteral("response")); auto url = QString::fromUtf8(QUrl::toPercentEncoding(fileInfo.absolutePath(), "/")); if (!url.endsWith(QChar('/'))) { url.append(QChar('/')); } const auto href = OCC::Utility::concatUrlPath(prefix, url).path(); xml.writeTextElement(davUri, QStringLiteral("href"), href); xml.writeStartElement(davUri, QStringLiteral("propstat")); xml.writeStartElement(davUri, QStringLiteral("prop")); if (fileInfo.isDir) { xml.writeStartElement(davUri, QStringLiteral("resourcetype")); xml.writeEmptyElement(davUri, QStringLiteral("collection")); xml.writeEndElement(); // resourcetype } else xml.writeEmptyElement(davUri, QStringLiteral("resourcetype")); auto gmtDate = fileInfo.lastModified.toUTC(); auto stringDate = QLocale::c().toString(gmtDate, QStringLiteral("ddd, dd MMM yyyy HH:mm:ss 'GMT'")); xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate); xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size)); xml.writeTextElement(davUri, QStringLiteral("getetag"), QStringLiteral("\"%1\"").arg(QString::fromLatin1(fileInfo.etag))); xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() ? QString(fileInfo.permissions.toString()) : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW")); xml.writeTextElement(ocUri, QStringLiteral("share-permissions"), QString::number(static_cast(OCC::SharePermissions(OCC::SharePermissionRead | OCC::SharePermissionUpdate | OCC::SharePermissionCreate | OCC::SharePermissionDelete | OCC::SharePermissionShare)))); xml.writeTextElement(ocUri, QStringLiteral("id"), QString::fromUtf8(fileInfo.fileId)); xml.writeTextElement(ocUri, QStringLiteral("fileid"), QString::fromUtf8(fileInfo.fileId)); xml.writeTextElement(ocUri, QStringLiteral("checksums"), QString::fromUtf8(fileInfo.checksums)); xml.writeTextElement(ocUri, QStringLiteral("privatelink"), href); xml.writeTextElement(ncUri, QStringLiteral("lock-owner"), fileInfo.lockOwnerId); xml.writeTextElement(ncUri, QStringLiteral("lock"), fileInfo.lockState == FileInfo::LockState::FileLocked ? QStringLiteral("1") : QStringLiteral("0")); xml.writeTextElement(ncUri, QStringLiteral("lock-owner-type"), fileInfo.lockOwnerId); xml.writeTextElement(ncUri, QStringLiteral("lock-owner-displayname"), fileInfo.lockOwnerId); xml.writeTextElement(ncUri, QStringLiteral("lock-owner-editor"), fileInfo.lockOwnerId); xml.writeTextElement(ncUri, QStringLiteral("lock-time"), QString::number(fileInfo.lockTime)); xml.writeTextElement(ncUri, QStringLiteral("lock-timeout"), QString::number(fileInfo.lockTimeout)); xml.writeTextElement(ncUri, QStringLiteral("is-encrypted"), fileInfo.isEncrypted ? QString::number(1) : QString::number(0)); buffer.write(fileInfo.extraDavProperties); xml.writeEndElement(); // prop xml.writeTextElement(davUri, QStringLiteral("status"), QStringLiteral("HTTP/1.1 200 OK")); xml.writeEndElement(); // propstat xml.writeEndElement(); // response }; writeFileResponse(*fileInfo); foreach (const FileInfo &childFileInfo, fileInfo->children) writeFileResponse(childFileInfo); xml.writeEndElement(); // multistatus xml.writeEndDocument(); QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); } void FakePropfindReply::respond() { setHeader(QNetworkRequest::ContentLengthHeader, payload.size()); setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/xml; charset=utf-8")); setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207); setFinished(true); emit metaDataChanged(); if (bytesAvailable()) emit readyRead(); emit finished(); } void FakePropfindReply::respond404() { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404); setError(InternalServerError, QStringLiteral("Not Found")); emit metaDataChanged(); emit finished(); } qint64 FakePropfindReply::bytesAvailable() const { return payload.size() + QIODevice::bytesAvailable(); } qint64 FakePropfindReply::readData(char *data, qint64 maxlen) { qint64 len = std::min(qint64 { payload.size() }, maxlen); std::copy(payload.cbegin(), payload.cbegin() + len, data); payload.remove(0, static_cast(len)); return len; } FakePutReply::FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); fileInfo = perform(remoteRootFileInfo, request, putPayload); QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); } FileInfo *FakePutReply::perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload) { QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isEmpty()); FileInfo *fileInfo = remoteRootFileInfo.find(fileName); if (fileInfo) { fileInfo->size = putPayload.size(); fileInfo->contentChar = putPayload.at(0); } else { // Assume that the file is filled with the same character fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0)); } fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true); return fileInfo; } void FakePutReply::respond() { emit uploadProgress(fileInfo->size, fileInfo->size); setRawHeader("OC-ETag", fileInfo->etag); setRawHeader("ETag", fileInfo->etag); setRawHeader("OC-FileID", fileInfo->fileId); setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); emit metaDataChanged(); emit finished(); } void FakePutReply::abort() { setError(OperationCanceledError, QStringLiteral("abort")); emit finished(); } FakePutMultiFileReply::FakePutMultiFileReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QString &contentType, const QByteArray &putPayload, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); _allFileInfo = performMultiPart(remoteRootFileInfo, request, putPayload, contentType); QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); } QVector FakePutMultiFileReply::performMultiPart(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload, const QString &contentType) { QVector result; auto stringPutPayload = QString::fromUtf8(putPayload); constexpr int boundaryPosition = sizeof("multipart/related; boundary="); const QString boundaryValue = QStringLiteral("--") + contentType.mid(boundaryPosition, contentType.length() - boundaryPosition - 1) + QStringLiteral("\r\n"); auto stringPutPayloadRef = QString{stringPutPayload}.left(stringPutPayload.size() - 2 - boundaryValue.size()); auto allParts = stringPutPayloadRef.split(boundaryValue, Qt::SkipEmptyParts); for (const auto &onePart : allParts) { auto headerEndPosition = onePart.indexOf(QStringLiteral("\r\n\r\n")); auto onePartHeaderPart = onePart.left(headerEndPosition); auto onePartBody = onePart.mid(headerEndPosition + 4, onePart.size() - headerEndPosition - 6); auto onePartHeaders = onePartHeaderPart.split(QStringLiteral("\r\n")); QMap allHeaders; for(auto oneHeader : onePartHeaders) { auto headerParts = oneHeader.split(QStringLiteral(": ")); allHeaders[headerParts.at(0)] = headerParts.at(1); } const auto fileName = allHeaders[QStringLiteral("X-File-Path")]; const auto modtime = allHeaders[QByteArrayLiteral("X-File-Mtime")].toLongLong(); Q_ASSERT(!fileName.isEmpty()); Q_ASSERT(modtime > 0); FileInfo *fileInfo = remoteRootFileInfo.find(fileName); if (fileInfo) { fileInfo->size = onePartBody.size(); fileInfo->contentChar = onePartBody.at(0).toLatin1(); } else { // Assume that the file is filled with the same character fileInfo = remoteRootFileInfo.create(fileName, onePartBody.size(), onePartBody.at(0).toLatin1()); } fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true); result.push_back(fileInfo); } return result; } void FakePutMultiFileReply::respond() { QJsonDocument reply; QJsonObject allFileInfoReply; qint64 totalSize = 0; std::for_each(_allFileInfo.begin(), _allFileInfo.end(), [&totalSize](const auto &fileInfo) { totalSize += fileInfo->size; }); for(auto fileInfo : qAsConst(_allFileInfo)) { QJsonObject fileInfoReply; fileInfoReply.insert("error", QStringLiteral("false")); fileInfoReply.insert("etag", QLatin1String{fileInfo->etag}); emit uploadProgress(fileInfo->size, totalSize); allFileInfoReply.insert(QChar('/') + fileInfo->path(), fileInfoReply); } reply.setObject(allFileInfoReply); _payload = reply.toJson(); setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); setFinished(true); if (bytesAvailable()) { emit readyRead(); } emit metaDataChanged(); emit finished(); } void FakePutMultiFileReply::abort() { setError(OperationCanceledError, QStringLiteral("abort")); emit finished(); } qint64 FakePutMultiFileReply::bytesAvailable() const { return _payload.size() + QIODevice::bytesAvailable(); } qint64 FakePutMultiFileReply::readData(char *data, qint64 maxlen) { qint64 len = std::min(qint64 { _payload.size() }, maxlen); std::copy(_payload.cbegin(), _payload.cbegin() + len, data); _payload.remove(0, static_cast(len)); return len; } FakeMkcolReply::FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isEmpty()); fileInfo = remoteRootFileInfo.createDir(fileName); if (!fileInfo) { abort(); return; } QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); } void FakeMkcolReply::respond() { setRawHeader("OC-FileId", fileInfo->fileId); setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); emit metaDataChanged(); emit finished(); } FakeDeleteReply::FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isEmpty()); remoteRootFileInfo.remove(fileName); QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); } void FakeDeleteReply::respond() { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204); emit metaDataChanged(); emit finished(); } FakeMoveReply::FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isEmpty()); QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination"))); Q_ASSERT(!dest.isEmpty()); remoteRootFileInfo.rename(fileName, dest); QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); } void FakeMoveReply::respond() { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); emit metaDataChanged(); emit finished(); } FakeGetReply::FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isEmpty()); fileInfo = remoteRootFileInfo.find(fileName); if (!fileInfo) { qDebug() << "url: " << request.url() << " fileName: " << fileName << " meh;"; } Q_ASSERT_X(fileInfo, Q_FUNC_INFO, "Could not find file on the remote"); QMetaObject::invokeMethod(this, &FakeGetReply::respond, Qt::QueuedConnection); } void FakeGetReply::respond() { if (aborted) { setError(OperationCanceledError, QStringLiteral("Operation Canceled")); emit metaDataChanged(); emit finished(); return; } if (!fileInfo) { setError(ContentNotFoundError, QStringLiteral("File Not Found")); emit metaDataChanged(); emit finished(); return; } payload = fileInfo->contentChar; size = fileInfo->size; setHeader(QNetworkRequest::ContentLengthHeader, size); setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); setRawHeader("OC-ETag", fileInfo->etag); setRawHeader("ETag", fileInfo->etag); setRawHeader("OC-FileId", fileInfo->fileId); emit metaDataChanged(); if (bytesAvailable()) emit readyRead(); emit finished(); } void FakeGetReply::abort() { setError(OperationCanceledError, QStringLiteral("Operation Canceled")); aborted = true; } qint64 FakeGetReply::bytesAvailable() const { if (aborted) return 0; return size + QIODevice::bytesAvailable(); } qint64 FakeGetReply::readData(char *data, qint64 maxlen) { qint64 len = std::min(qint64 { size }, maxlen); std::fill_n(data, len, payload); size -= len; return len; } FakeGetWithDataReply::FakeGetWithDataReply(FileInfo &remoteRootFileInfo, const QByteArray &data, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); Q_ASSERT(!data.isEmpty()); payload = data; QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isEmpty()); fileInfo = remoteRootFileInfo.find(fileName); QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); if (request.hasRawHeader("Range")) { const QString range = QString::fromUtf8(request.rawHeader("Range")); const QRegularExpression bytesPattern(QStringLiteral("bytes=(?\\d+)-(?\\d+)")); const QRegularExpressionMatch match = bytesPattern.match(range); if (match.hasMatch()) { const int start = match.captured(QStringLiteral("start")).toInt(); const int end = match.captured(QStringLiteral("end")).toInt(); payload = payload.mid(start, end - start + 1); } } } void FakeGetWithDataReply::respond() { if (aborted) { setError(OperationCanceledError, QStringLiteral("Operation Canceled")); emit metaDataChanged(); emit finished(); return; } setHeader(QNetworkRequest::ContentLengthHeader, payload.size()); setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); setRawHeader("OC-ETag", fileInfo->etag); setRawHeader("ETag", fileInfo->etag); setRawHeader("OC-FileId", fileInfo->fileId); emit metaDataChanged(); if (bytesAvailable()) emit readyRead(); emit finished(); } void FakeGetWithDataReply::abort() { setError(OperationCanceledError, QStringLiteral("Operation Canceled")); aborted = true; } qint64 FakeGetWithDataReply::bytesAvailable() const { if (aborted) return 0; return payload.size() - offset + QIODevice::bytesAvailable(); } qint64 FakeGetWithDataReply::readData(char *data, qint64 maxlen) { qint64 len = std::min(payload.size() - offset, quint64(maxlen)); std::memcpy(data, payload.constData() + offset, len); offset += len; return len; } FakeChunkMoveReply::FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakeReply { parent } { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); fileInfo = perform(uploadsFileInfo, remoteRootFileInfo, request); if (!fileInfo) { QTimer::singleShot(0, this, &FakeChunkMoveReply::respondPreconditionFailed); } else { QTimer::singleShot(0, this, &FakeChunkMoveReply::respond); } } FileInfo *FakeChunkMoveReply::perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request) { QString source = getFilePathFromUrl(request.url()); Q_ASSERT(!source.isEmpty()); Q_ASSERT(source.endsWith(QLatin1String("/.file"))); source = source.left(source.length() - static_cast(qstrlen("/.file"))); auto sourceFolder = uploadsFileInfo.find(source); Q_ASSERT(sourceFolder); Q_ASSERT(sourceFolder->isDir); int count = 0; qlonglong size = 0; char payload = '\0'; QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination"))); Q_ASSERT(!fileName.isEmpty()); // Compute the size and content from the chunks if possible const auto childrenKeys = sourceFolder->children.keys(); for (auto chunkName : childrenKeys) { auto &x = sourceFolder->children[chunkName]; Q_ASSERT(!x.isDir); Q_ASSERT(x.size > 0); // There should not be empty chunks size += x.size; Q_ASSERT(!payload || payload == x.contentChar); payload = x.contentChar; ++count; } Q_ASSERT(sourceFolder->children.count() == count); // There should not be holes or extra files // NOTE: This does not actually assemble the file data from the chunks! FileInfo *fileInfo = remoteRootFileInfo.find(fileName); if (fileInfo) { // The client should put this header Q_ASSERT(request.hasRawHeader("If")); // And it should condition on the destination file auto start = QByteArray("<" + request.rawHeader("Destination") + ">"); Q_ASSERT(request.rawHeader("If").startsWith(start)); if (request.rawHeader("If") != start + " ([\"" + fileInfo->etag + "\"])") { return nullptr; } fileInfo->size = size; fileInfo->contentChar = payload; } else { Q_ASSERT(!request.hasRawHeader("If")); // Assume that the file is filled with the same character fileInfo = remoteRootFileInfo.create(fileName, size, payload); } fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true); return fileInfo; } void FakeChunkMoveReply::respond() { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201); setRawHeader("OC-ETag", fileInfo->etag); setRawHeader("ETag", fileInfo->etag); setRawHeader("OC-FileId", fileInfo->fileId); emit metaDataChanged(); emit finished(); } void FakeChunkMoveReply::respondPreconditionFailed() { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 412); setError(InternalServerError, QStringLiteral("Precondition Failed")); emit metaDataChanged(); emit finished(); } void FakeChunkMoveReply::abort() { setError(OperationCanceledError, QStringLiteral("abort")); emit finished(); } FakePayloadReply::FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &body, QObject *parent) : FakePayloadReply(op, request, body, FakePayloadReply::defaultDelay, parent) { } FakePayloadReply::FakePayloadReply( QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &body, int delay, QObject *parent) : FakeReply{parent} , _body(body) { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); QTimer::singleShot(delay, this, &FakePayloadReply::respond); } void FakePayloadReply::respond() { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); setHeader(QNetworkRequest::ContentLengthHeader, _body.size()); for (auto it = _additionalHeaders.constKeyValueBegin(); it != _additionalHeaders.constKeyValueEnd(); ++it) { setHeader(it->first, it->second); } emit metaDataChanged(); emit readyRead(); setFinished(true); emit finished(); } qint64 FakePayloadReply::readData(char *buf, qint64 max) { max = qMin(max, _body.size()); memcpy(buf, _body.constData(), max); _body = _body.mid(max); return max; } qint64 FakePayloadReply::bytesAvailable() const { return _body.size(); } FakeErrorReply::FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent, int httpErrorCode, const QByteArray &body) : FakeReply { parent } , _body(body) { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpErrorCode); setError(InternalServerError, QStringLiteral("Internal Server Fake Error")); QMetaObject::invokeMethod(this, &FakeErrorReply::respond, Qt::QueuedConnection); } void FakeErrorReply::respond() { emit metaDataChanged(); emit readyRead(); // finishing can come strictly after readyRead was called QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished); } void FakeErrorReply::slotSetFinished() { setFinished(true); emit finished(); } qint64 FakeErrorReply::readData(char *buf, qint64 max) { max = qMin(max, _body.size()); memcpy(buf, _body.constData(), max); _body = _body.mid(max); return max; } qint64 FakeErrorReply::bytesAvailable() const { return _body.size(); } FakeHangingReply::FakeHangingReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakeReply(parent) { setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); } void FakeHangingReply::abort() { // Follow more or less the implementation of QNetworkReplyImpl::abort close(); setError(OperationCanceledError, tr("Operation canceled")); emit errorOccurred(OperationCanceledError); setFinished(true); emit finished(); } FakeQNAM::FakeQNAM(FileInfo initialRoot) : _remoteRootFileInfo { std::move(initialRoot) } { setCookieJar(new OCC::CookieJar); } QJsonObject FakeQNAM::forEachReplyPart(QIODevice *outgoingData, const QString &contentType, std::function &)> replyFunction) { auto fullReply = QJsonObject{}; auto putPayload = outgoingData->peek(outgoingData->bytesAvailable()); outgoingData->reset(); auto stringPutPayload = QString::fromUtf8(putPayload); constexpr int boundaryPosition = sizeof("multipart/related; boundary="); const QString boundaryValue = QStringLiteral("--") + contentType.mid(boundaryPosition, contentType.length() - boundaryPosition - 1) + QStringLiteral("\r\n"); auto stringPutPayloadRef = QString{stringPutPayload}.left(stringPutPayload.size() - 2 - boundaryValue.size()); auto allParts = stringPutPayloadRef.split(boundaryValue, Qt::SkipEmptyParts); for (const auto &onePart : qAsConst(allParts)) { auto headerEndPosition = onePart.indexOf(QStringLiteral("\r\n\r\n")); auto onePartHeaderPart = onePart.left(headerEndPosition); auto onePartHeaders = onePartHeaderPart.split(QStringLiteral("\r\n")); QMap allHeaders; for(const auto &oneHeader : qAsConst(onePartHeaders)) { auto headerParts = oneHeader.split(QStringLiteral(": ")); allHeaders[headerParts.at(0)] = headerParts.at(1).toLatin1(); } auto reply = replyFunction(allHeaders); if (reply.contains(QStringLiteral("error")) && reply.contains(QStringLiteral("etag"))) { fullReply.insert(allHeaders[QStringLiteral("X-File-Path")], reply); } } return fullReply; } QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) { QNetworkReply *reply = nullptr; auto newRequest = request; newRequest.setRawHeader("X-Request-ID", OCC::AccessManager::generateRequestId()); auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString(); if (_override) { qDebug() << "Using override!"; if (auto _reply = _override(op, newRequest, outgoingData)) { reply = _reply; } } if (!reply) { qDebug() << newRequest.url(); reply = overrideReplyWithError(getFilePathFromUrl(newRequest.url()), op, newRequest); } if (!reply) { const bool isUpload = newRequest.url().path().startsWith(sUploadUrl.path()); FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo; auto verb = newRequest.attribute(QNetworkRequest::CustomVerbAttribute); if (verb == QLatin1String("PROPFIND")) { // Ignore outgoingData always returning somethign good enough, works for now. reply = new FakePropfindReply { info, op, newRequest, this }; } else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation) { reply = new FakeGetReply { info, op, newRequest, this }; } else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation) { if (request.hasRawHeader(QByteArrayLiteral("X-OC-Mtime")) && request.rawHeader(QByteArrayLiteral("X-OC-Mtime")).toLongLong() <= 0) { reply = new FakeErrorReply { op, request, this, 500 }; } else { reply = new FakePutReply { info, op, newRequest, outgoingData->readAll(), this }; } } else if (verb == QLatin1String("MKCOL")) { reply = new FakeMkcolReply { info, op, newRequest, this }; } else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation) { reply = new FakeDeleteReply { info, op, newRequest, this }; } else if (verb == QLatin1String("MOVE") && !isUpload) { reply = new FakeMoveReply { info, op, newRequest, this }; } else if (verb == QLatin1String("MOVE") && isUpload) { reply = new FakeChunkMoveReply { info, _remoteRootFileInfo, op, newRequest, this }; } else if (verb == QLatin1String("POST") || op == QNetworkAccessManager::PostOperation) { if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) { reply = new FakePutMultiFileReply { info, op, newRequest, contentType, outgoingData->readAll(), this }; } } else if (verb == QLatin1String("LOCK") || verb == QLatin1String("UNLOCK")) { reply = new FakeFileLockReply{info, op, newRequest, this}; } else { qDebug() << verb << outgoingData; Q_UNREACHABLE(); } } OCC::HttpLogger::logRequest(reply, op, outgoingData); return reply; } QNetworkReply * FakeQNAM::overrideReplyWithError(QString fileName, QNetworkAccessManager::Operation op, QNetworkRequest newRequest) { QNetworkReply *reply = nullptr; Q_ASSERT(!fileName.isNull()); if (_errorPaths.contains(fileName)) { reply = new FakeErrorReply { op, newRequest, this, _errorPaths[fileName] }; } return reply; } FakeFolder::FakeFolder(const FileInfo &fileTemplate, const OCC::Optional &localFileInfo, const QString &remotePath) : _localModifier(_tempDir.path()) { // Needs to be done once OCC::SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0); OCC::Logger::instance()->setLogFile(QStringLiteral("-")); OCC::Logger::instance()->addLogRule({ QStringLiteral("sync.httplogger=true") }); QDir rootDir { _tempDir.path() }; qDebug() << "FakeFolder operating on" << rootDir; if (localFileInfo) { toDisk(rootDir, *localFileInfo); } else { toDisk(rootDir, fileTemplate); } _fakeQnam = new FakeQNAM(fileTemplate); _account = OCC::Account::create(); _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud"))); _account->setCredentials(new FakeCredentials { _fakeQnam }); _account->setDavDisplayName(QStringLiteral("fakename")); _account->setServerVersion(QStringLiteral("10.0.0")); _journalDb = std::make_unique(localPath() + QStringLiteral(".sync_test.db")); _syncEngine = std::make_unique(_account, localPath(), OCC::SyncOptions{}, remotePath, _journalDb.get()); // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it) _syncEngine->excludedFiles().addManualExclude(QStringLiteral("]*.~*")); // handle aboutToRemoveAllFiles with a timeout in case our test does not handle it QObject::connect(_syncEngine.get(), &OCC::SyncEngine::aboutToRemoveAllFiles, _syncEngine.get(), [this](OCC::SyncFileItem::Direction, std::function callback) { QTimer::singleShot(1 * 1000, _syncEngine.get(), [callback] { callback(false); }); }); // Ensure we have a valid VfsOff instance "running" switchToVfs(_syncEngine->syncOptions()._vfs); // A new folder will update the local file state database on first sync. // To have a state matching what users will encounter, we have to a sync // using an identical local/remote file tree first. ENFORCE(syncOnce()); } void FakeFolder::switchToVfs(QSharedPointer vfs) { auto opts = _syncEngine->syncOptions(); opts._vfs->stop(); QObject::disconnect(_syncEngine.get(), nullptr, opts._vfs.data(), nullptr); opts._vfs = vfs; _syncEngine->setSyncOptions(opts); OCC::VfsSetupParams vfsParams; vfsParams.filesystemPath = localPath(); vfsParams.remotePath = QLatin1Char('/'); vfsParams.account = _account; vfsParams.journal = _journalDb.get(); vfsParams.providerName = QStringLiteral("OC-TEST"); vfsParams.providerVersion = QStringLiteral("0.1"); QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() { vfs->stop(); vfs->unregisterFolder(); }); vfs->start(vfsParams); } FileInfo FakeFolder::currentLocalState() { QDir rootDir { _tempDir.path() }; FileInfo rootTemplate; fromDisk(rootDir, rootTemplate); rootTemplate.fixupParentPathRecursively(); return rootTemplate; } QString FakeFolder::localPath() const { // SyncEngine wants a trailing slash if (_tempDir.path().endsWith(QLatin1Char('/'))) return _tempDir.path(); return _tempDir.path() + QLatin1Char('/'); } void FakeFolder::scheduleSync() { // Have to be done async, else, an error before exec() does not terminate the event loop. QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection); } void FakeFolder::execUntilBeforePropagation() { QSignalSpy spy(_syncEngine.get(), &OCC::SyncEngine::aboutToPropagate); QVERIFY(spy.wait()); } void FakeFolder::execUntilItemCompleted(const QString &relativePath) { QSignalSpy spy(_syncEngine.get(), &OCC::SyncEngine::itemCompleted); QElapsedTimer t; t.start(); while (t.elapsed() < 5000) { spy.clear(); QVERIFY(spy.wait()); for (const QList &args : spy) { auto item = args[0].value(); if (item->destination() == relativePath) return; } } QVERIFY(false); } void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi) { for(const auto &child : templateFi.children) { if (child.isDir) { QDir subDir(dir); dir.mkdir(child.name); subDir.cd(child.name); toDisk(subDir, child); } else { QFile file { dir.filePath(child.name) }; file.open(QFile::WriteOnly); file.write(QByteArray {}.fill(child.contentChar, child.size)); file.close(); OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified)); } } } void FakeFolder::fromDisk(QDir &dir, FileInfo &templateFi) { for(const auto &diskChild : dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { if (diskChild.isDir()) { QDir subDir = dir; subDir.cd(diskChild.fileName()); FileInfo &subFi = templateFi.children[diskChild.fileName()] = FileInfo { diskChild.fileName() }; fromDisk(subDir, subFi); } else { QFile f { diskChild.filePath() }; f.open(QFile::ReadOnly); auto content = f.read(1); if (content.size() == 0) { qWarning() << "Empty file at:" << diskChild.filePath(); continue; } char contentChar = content.at(0); templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar, diskChild.lastModified()}); } } } static FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components) { if (components.isEmpty()) return base; auto childName = components.pathRoot(); auto it = base.children.find(childName); if (it != base.children.end()) { return findOrCreateDirs(*it, components.subComponents()); } auto &newDir = base.children[childName] = FileInfo { childName }; newDir.parentPath = base.path(); return findOrCreateDirs(newDir, components.subComponents()); } FileInfo FakeFolder::dbState() const { FileInfo result; [[maybe_unused]] const auto journalDbResult =_journalDb->getFilesBelowPath("", [&](const OCC::SyncJournalFileRecord &record) { auto components = PathComponents(record.path()); auto &parentDir = findOrCreateDirs(result, components.parentDirComponents()); auto name = components.fileName(); auto &item = parentDir.children[name]; item.name = name; item.parentPath = parentDir.path(); item.size = record._fileSize; item.isDir = record._type == ItemTypeDirectory; item.permissions = record._remotePerm; item.etag = record._etag; item.lastModified = OCC::Utility::qDateTimeFromTime_t(record._modtime); item.fileId = record._fileId; item.checksums = record._checksumHeader; // item.contentChar can't be set from the db }); return result; } OCC::SyncFileItemPtr ItemCompletedSpy::findItem(const QString &path) const { for (const QList &args : *this) { auto item = args[0].value(); if (item->destination() == path) return item; } return OCC::SyncFileItemPtr::create(); } OCC::SyncFileItemPtr ItemCompletedSpy::findItemWithExpectedRank(const QString &path, int rank) const { Q_ASSERT(size() > rank); Q_ASSERT(!(*this)[rank].isEmpty()); auto item = (*this)[rank][0].value(); if (item->destination() == path) { return item; } else { return OCC::SyncFileItemPtr::create(); } } FakeReply::FakeReply(QObject *parent) : QNetworkReply(parent) { setRawHeader(QByteArrayLiteral("Date"), QDateTime::currentDateTimeUtc().toString(Qt::RFC2822Date).toUtf8()); } FakeReply::~FakeReply() = default; FakeJsonErrorReply::FakeJsonErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent, int httpErrorCode, const QJsonDocument &reply) : FakeErrorReply{ op, request, parent, httpErrorCode, reply.toJson() } { } FakeFileLockReply::FakeFileLockReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakePropfindReply(remoteRootFileInfo, op, request, parent) { const auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute); setRequest(request); setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isNull()); // for root, it should be empty FileInfo *fileInfo = remoteRootFileInfo.find(fileName); if (!fileInfo) { QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection); return; } const QString prefix = request.url().path().left(request.url().path().size() - fileName.size()); // Don't care about the request and just return a full propfind const QString davUri { QStringLiteral("DAV:") }; const QString ocUri { QStringLiteral("http://owncloud.org/ns") }; const QString ncUri { QStringLiteral("http://nextcloud.org/ns") }; payload.clear(); QBuffer buffer { &payload }; buffer.open(QIODevice::WriteOnly); QXmlStreamWriter xml(&buffer); xml.writeNamespace(davUri, QStringLiteral("d")); xml.writeNamespace(ocUri, QStringLiteral("oc")); xml.writeNamespace(ncUri, QStringLiteral("nc")); xml.writeStartDocument(); xml.writeStartElement(davUri, QStringLiteral("prop")); xml.writeTextElement(ncUri, QStringLiteral("lock"), verb == QStringLiteral("LOCK") ? "1" : "0"); xml.writeTextElement(ncUri, QStringLiteral("lock-owner-type"), QString::number(0)); xml.writeTextElement(ncUri, QStringLiteral("lock-owner"), QStringLiteral("admin")); xml.writeTextElement(ncUri, QStringLiteral("lock-owner-displayname"), QStringLiteral("John Doe")); xml.writeTextElement(ncUri, QStringLiteral("lock-owner-editor"), {}); xml.writeTextElement(ncUri, QStringLiteral("lock-time"), QString::number(1234560)); xml.writeTextElement(ncUri, QStringLiteral("lock-timeout"), QString::number(1800)); xml.writeEndElement(); // prop xml.writeEndDocument(); }