Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/libsync/filesystem.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2014 ownCloud GmbH
Expand All @@ -16,11 +16,14 @@
#include <QDir>
#include <QDirIterator>
#include <QCoreApplication>
#include <QRegularExpression>

#include <array>
#include <map>
#include <optional>
#include <ranges>
#include <string_view>
#include <vector>

#ifdef Q_OS_WIN
#include <securitybaseapi.h>
Expand Down Expand Up @@ -80,6 +83,96 @@
});
}

// Unlike Office (`~$…`) and LibreOffice (`.~lock.…#`) lock files, Adobe lock file
// names do not encode the guarded document's own extension, only its base name.
// The guarded document has to be located among the lock file's siblings.
// - `idlk`: InDesign documents (`indd`) and InCopy stories (`icml`).
// - `prlock`: Premiere Pro projects (`prproj`).
static const std::map<std::string_view, std::vector<std::string_view>> adobeLockFileDocumentExtensions = {

Check warning on line 91 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the redundant "static" specifier.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTT&open=AZ9vHFPHFY_QJQGNpnTT&pullRequest=10367
{"idlk", {"indd", "icml"}},
{"prlock", {"prproj"}},
};

bool isAdobeLockFileExtension(const QString &path)

Check warning on line 96 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:96:6 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
const auto suffix = QFileInfo{path}.suffix().toLower().toStdString();
return adobeLockFileDocumentExtensions.contains(suffix);
}

// Returns the candidate document extensions guarded by the given Adobe lock file
// extension (lowercased, without the dot), in lookup order. Returns std::nullopt
// if the extension is not a recognised Adobe lock file extension.
std::optional<QList<QString>> adobeDocumentExtensionsFor(const QString &lockExtension)

Check warning on line 105 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:105:31 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
const auto lockExt = lockExtension.toStdString();
const auto it = adobeLockFileDocumentExtensions.find(lockExt);

Check warning on line 108 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:108:16 [readability-identifier-length]

variable name 'it' is too short, expected at least 3 characters
if (it == adobeLockFileDocumentExtensions.cend()) {
return std::nullopt;
}
QList<QString> result;

Check warning on line 112 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:112:20 [cppcoreguidelines-init-variables]

variable 'result' is not initialized
result.reserve(static_cast<int>(it->second.size()));
for (const auto &documentExtension : it->second) {
result.append(QString::fromStdString(std::string(documentExtension)));
}
return result;
}

// Parses the document base name embedded in an Adobe lock file name.
// - InDesign / InCopy (.idlk): `Test` is extracted from `~Test~0kjyv(.idlk`.
// - Premiere Pro (.prlock): `Test` is extracted from `Test.prlock`.
// Adobe lock file names drop the guarded document's own extension, so only the
// base name can be recovered here. Returns std::nullopt if \a lockExtension is
// not a recognised Adobe lock file extension, or if \a lockFileName does not
// match the naming pattern expected for it.
std::optional<QString> adobeLockFileDocumentBaseName(const QString &lockFileName, const QString &lockExtension)

Check warning on line 127 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:127:54 [bugprone-easily-swappable-parameters]

2 adjacent parameters of 'adobeLockFileDocumentBaseName' of similar type ('const int &') are easily swapped by mistake

Check warning on line 127 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:127:24 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
static const QRegularExpression idlkPattern(QStringLiteral(R"(^~(?<base>.+)~[^~]*\(\.idlk$)"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression prlockPattern(QStringLiteral(R"(^(?<base>.+)\.prlock$)"),
QRegularExpression::CaseInsensitiveOption);

const auto pattern = lockExtension == QLatin1String("idlk") ? &idlkPattern
: lockExtension == QLatin1String("prlock") ? &prlockPattern
: nullptr;

Check warning on line 136 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested conditional operator into an independent statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTU&open=AZ9vHFPHFY_QJQGNpnTU&pullRequest=10367
if (!pattern) {
return std::nullopt;
}

const auto match = pattern->match(lockFileName);
if (!match.hasMatch()) {
return std::nullopt;
}
return match.captured(QStringLiteral("base"));
}

// Resolve the document guarded by an Adobe lock file by matching a sibling file
// in the same directory by base name and expected document extension. Returns
// the guarded document's absolute file path, or an empty string if no matching
// document is found.
std::optional<QString> adobeLockFileTargetFilePath(const QString &lockFilePath)

Check warning on line 152 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:152:24 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
const QFileInfo lockFileInfo{lockFilePath};

Check warning on line 154 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:154:21 [cppcoreguidelines-init-variables]

variable 'lockFileInfo' is not initialized
const auto lockExtension = QFileInfo{lockFileInfo.fileName()}.suffix().toLower();
const auto documentExtensions = adobeDocumentExtensionsFor(lockExtension);
if (!documentExtensions || documentExtensions->isEmpty()) {
return std::nullopt;
}

const auto baseName = adobeLockFileDocumentBaseName(lockFileInfo.fileName(), lockExtension);
if (!baseName || baseName->isEmpty()) {
return std::nullopt;
}

const QDir dir = lockFileInfo.dir();

Check warning on line 166 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:166:16 [cppcoreguidelines-init-variables]

variable 'dir' is not initialized
for (const auto &documentExtension : *documentExtensions) {
const auto candidatePath = dir.absoluteFilePath(*baseName + QLatin1Char('.') + documentExtension);
if (QFileInfo::exists(candidatePath)) {
return candidatePath;
}
}
return std::nullopt;
}

// iterates through the dirPath to find the matching fileName
QString findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName)
{
Expand All @@ -88,10 +181,10 @@
const auto entryList = dir.entryInfoList(QDir::Files);
for (const auto &candidateUnlockedFileInfo : entryList) {
const auto candidateUnlockFileName = candidateUnlockedFileInfo.fileName();
const auto lockFilePatternFoundIt = std::find_if(std::cbegin(lockFilePatterns), std::cend(lockFilePatterns), [&candidateUnlockFileName](std::string_view pattern) {
return candidateUnlockFileName.contains(QString::fromStdString(std::string(pattern)));
});

Check warning on line 186 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace with the version of "std::ranges::find_if" that takes a range.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTW&open=AZ9vHFPHFY_QJQGNpnTW&pullRequest=10367
if (lockFilePatternFoundIt != std::cend(lockFilePatterns)) {

Check warning on line 187 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "lockFilePatternFoundIt" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTV&open=AZ9vHFPHFY_QJQGNpnTV&pullRequest=10367
continue;
}
if (candidateUnlockFileName.contains(lockFileName)) {
Expand Down Expand Up @@ -129,6 +222,14 @@
return QStringLiteral(".") + suffix;
}

// Adobe lock files (.idlk / .prlock) are identified by extension, not prefix.
const auto suffix = QFileInfo{pathSplit.last()}.suffix().toLower().toStdString();
if (adobeLockFileDocumentExtensions.contains(suffix)) {

Check warning on line 227 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "suffix" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTY&open=AZ9vHFPHFY_QJQGNpnTY&pullRequest=10367
const auto pattern = QStringLiteral(".") + QString::fromStdString(suffix);
qCDebug(OCC::lcFileSystem) << "Found an Adobe lock file with extension:" << pattern << "in path:" << path;
return pattern;
}

return {};
}

Expand All @@ -139,11 +240,20 @@
return std::find(std::cbegin(officeFileExtensions), std::cend(officeFileExtensions), extension) != std::cend(officeFileExtensions);
}

bool FileSystem::isMatchingAutoCADDocumentExtension(const QString &path)

Check warning on line 243 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9-n6EzndcBkYZDTQCW&open=AZ9-n6EzndcBkYZDTQCW&pullRequest=10367
{
return QFileInfo{path}.suffix().toLower().toStdString() == autoCADDocumentExtension;
}

bool FileSystem::isMatchingAdobeDocumentExtension(const QString &path)

Check warning on line 248 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:248:18 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
const auto pathSplit = path.split(QLatin1Char('.'));
const auto extension = pathSplit.size() > 1 ? pathSplit.last().toLower().toStdString() : std::string{};
return std::ranges::any_of(adobeLockFileDocumentExtensions, [&extension](const auto &entry) {
return std::ranges::any_of(entry.second, [&extension](const auto &documentExtension) { return documentExtension == extension; });
});
}

FileSystem::FileLockingInfo FileSystem::lockFileTargetFilePath(const QString &lockFilePath, const QString &lockFileNamePattern)
{
FileLockingInfo result;
Expand All @@ -155,7 +265,7 @@
// the watcher does not prematurely unlock the document.
if (isAutoCADLockFileExtension(lockFilePath)) {
const auto targetPath = autoCADLockFileTargetFilePath(lockFilePath);
if (targetPath) {

Check warning on line 268 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "targetPath" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9-n6EzndcBkYZDTQCX&open=AZ9-n6EzndcBkYZDTQCX&pullRequest=10367
result.path = *targetPath;
if (QFile::exists(lockFilePath)) {
result.type = FileLockingInfo::Type::Locked;
Expand All @@ -166,6 +276,18 @@
return result;
}

// Adobe lock files (.idlk / .prlock) are resolved by sibling lookup — the lock
// file name carries only the document base name, not its extension.
if (isAdobeLockFileExtension(lockFilePath)) {
const auto adobeResolvedPath = adobeLockFileTargetFilePath(lockFilePath);
if (adobeResolvedPath) {
result.path = *adobeResolvedPath;
if (!result.path.isEmpty())
result.type = QFile::exists(lockFilePath) ? FileLockingInfo::Type::Locked : FileLockingInfo::Type::Unlocked;
}
return result;
}

if (lockFileNamePattern.isEmpty()) {
return result;
}
Expand All @@ -179,19 +301,19 @@

auto extensionSanitized = lockFilePathWithoutPrefixSplit.takeLast().toStdString();
// remove possible non-alphabetical characters at the end of the extension
extensionSanitized.erase(std::remove_if(extensionSanitized.begin(),
extensionSanitized.end(),
[](const auto &ch) {
return !std::isalnum(ch);
}),

Check warning on line 308 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace with the version of "std::ranges::remove_if" that takes a range.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTe&open=AZ9vHFPHFY_QJQGNpnTe&pullRequest=10367
extensionSanitized.end());

Check warning on line 309 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this erase-remove idiom with a "std::erase_if" call.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTd&open=AZ9vHFPHFY_QJQGNpnTd&pullRequest=10367

lockFilePathWithoutPrefixSplit.push_back(QString::fromStdString(extensionSanitized));
const auto lockFilePathWithoutPrefixNew = lockFilePathWithoutPrefixSplit.join(QLatin1Char('.'));

qCDebug(lcFileSystem) << "Assumed locked/unlocked file path" << lockFilePathWithoutPrefixNew << "Going to try to find matching file";
auto splitFilePath = lockFilePathWithoutPrefixNew.split(QLatin1Char('/'));
if (splitFilePath.size() > 1) {

Check warning on line 316 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "splitFilePath" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTc&open=AZ9vHFPHFY_QJQGNpnTc&pullRequest=10367
const auto lockFileNameWithoutPrefix = splitFilePath.takeLast();
// some software will modify lock file name such that it does not correspond to original file (removing some symbols from the name, so we will
// search for a matching file
Expand Down Expand Up @@ -271,7 +393,7 @@
FilePermissionsRestore restore(filename, FileSystem::FolderPermissions::ReadWrite);
#endif

int rc = c_utimes(filename, modTime);

Check warning on line 396 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "rc" of type "int" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTf&open=AZ9vHFPHFY_QJQGNpnTf&pullRequest=10367
if (rc != 0) {
qCWarning(lcFileSystem) << "Error setting mtime for" << filename
<< "failed: rc" << rc << ", errno:" << errno;
Expand Down Expand Up @@ -329,14 +451,14 @@
}

// Code inspired from Qt5's QDir::removeRecursively
bool FileSystem::removeRecursively(const QString &path,

Check failure on line 454 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 50 to the 25 allowed.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTg&open=AZ9vHFPHFY_QJQGNpnTg&pullRequest=10367
const std::function<void(const QString &path, bool isDir)> &onDeleted,
QStringList *errors,

Check warning on line 456 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "errors" of type "class QList<class QString> *" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTi&open=AZ9vHFPHFY_QJQGNpnTi&pullRequest=10367
const std::function<void(const QString &path, bool isDir)> &onError,
const std::function<bool (const QString &, QString*)> &customDeleteFunction)
{
if (!FileSystem::setFolderPermissions(path, FileSystem::FolderPermissions::ReadWrite)) {
if (onError) {

Check warning on line 461 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this "if" statement with the enclosing one.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTj&open=AZ9vHFPHFY_QJQGNpnTj&pullRequest=10367
onError(path, true);
}
}
Expand Down Expand Up @@ -372,7 +494,7 @@
errors->append(QCoreApplication::translate("FileSystem", "Error removing \"%1\": %2")
.arg(QDir::toNativeSeparators(di.filePath()), removeError));
}
if (onError) {

Check failure on line 497 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTh&open=AZ9vHFPHFY_QJQGNpnTh&pullRequest=10367
onError(di.filePath(), false);
}
qCWarning(lcFileSystem) << "Error removing " << di.filePath() << ':' << removeError;
Expand Down Expand Up @@ -432,7 +554,7 @@
bool FileSystem::getInode(const QString &filename, quint64 *inode)
{
csync_file_stat_t fs;
if (csync_vio_local_stat(filename, &fs, true) == 0) {

Check warning on line 557 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "fs" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTm&open=AZ9vHFPHFY_QJQGNpnTm&pullRequest=10367
*inode = fs.inode;
return true;
}
Expand All @@ -457,7 +579,7 @@

permissionsDidChange = true;
#else
const auto stdStrPath = path.toStdWString();

Check warning on line 582 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change type of "stdStrPath" to "std::filesystem::path".

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTo&open=AZ9vHFPHFY_QJQGNpnTo&pullRequest=10367

try
{
Expand All @@ -466,7 +588,7 @@
const auto currentPermissions = std::filesystem::status(stdStrPath).permissions();
qCDebug(lcFileSystem()).nospace() << "current permissions path=" << path << " perms=" << Qt::showbase << Qt::oct << static_cast<int>(currentPermissions);

switch (permissions) {

Check failure on line 591 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a "default" case to this switch statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTp&open=AZ9vHFPHFY_QJQGNpnTp&pullRequest=10367
case OCC::FileSystem::FolderPermissions::ReadOnly: {
qCDebug(lcFileSystem()).nospace() << "ensuring folder is read only path=" << path;

Expand Down Expand Up @@ -651,7 +773,7 @@
{
qCWarning(lcFileSystem()) << "exception when modifying folder permissions" << e.what() << "- path:" << path;
}
catch (...)

Check warning on line 776 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"catch" a specific exception type.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTr&open=AZ9vHFPHFY_QJQGNpnTr&pullRequest=10367
{
qCWarning(lcFileSystem()) << "exception when modifying folder permissions - path:" << path;
}
Expand All @@ -664,7 +786,7 @@
}
}

bool FileSystem::uncheckedRenameReplace(const QString &originFileName, const QString &destinationFileName, QString *errorString)

Check warning on line 789 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "errorString" of type "class QString *" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTt&open=AZ9vHFPHFY_QJQGNpnTt&pullRequest=10367
{
#ifndef Q_OS_WIN
bool success = false;
Expand All @@ -673,8 +795,8 @@
// Qt 5.1 has QSaveFile::renameOverwrite we could use.
// ### FIXME
success = true;
bool destExists = fileExists(destinationFileName);

Check warning on line 798 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "destExists" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTu&open=AZ9vHFPHFY_QJQGNpnTu&pullRequest=10367
if (destExists && !QFile::remove(destinationFileName)) {

Check warning on line 799 in src/libsync/filesystem.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "destExists" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFPHFY_QJQGNpnTs&open=AZ9vHFPHFY_QJQGNpnTs&pullRequest=10367
*errorString = orig.errorString();
qCWarning(lcFileSystem) << "Target file could not be removed.";
success = false;
Expand Down
2 changes: 2 additions & 0 deletions src/libsync/filesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#pragma once

#include "config.h"

Check failure on line 9 in src/libsync/filesystem.h

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.h:9:10 [clang-diagnostic-error]

'config.h' file not found

#include "owncloudlib.h"
#include "common/filesystembase.h"
Expand All @@ -33,7 +33,7 @@
* @brief This file contains file system helper
*/
namespace FileSystem {
class OWNCLOUDSYNC_EXPORT FilePermissionsRestore {

Check failure on line 36 in src/libsync/filesystem.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Customize this class' copy constructor to participate in resource management. Customize or delete its copy assignment operator. Also consider whether move operations should be customized.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFR-FY_QJQGNpnTy&open=AZ9vHFR-FY_QJQGNpnTy&pullRequest=10367
public:
explicit FilePermissionsRestore(const QString &path,
FileSystem::FolderPermissions temporaryPermissions);
Expand All @@ -58,6 +58,8 @@
bool OWNCLOUDSYNC_EXPORT isMatchingOfficeFileExtension(const QString &path);
// check if it is an AutoCAD document (by .dwg extension), ONLY call it for files
bool OWNCLOUDSYNC_EXPORT isMatchingAutoCADDocumentExtension(const QString &path);
// check if it is an Adobe document guarded by an Adobe lock file (by extension), ONLY call it for files
bool OWNCLOUDSYNC_EXPORT isMatchingAdobeDocumentExtension(const QString &path);
// finds and fetches FileLockingInfo for the corresponding file that we are locking/unlocking
FileLockingInfo OWNCLOUDSYNC_EXPORT lockFileTargetFilePath(const QString &lockFilePath, const QString &lockFileNamePattern);
// lists all files matching a lockfile pattern in dirPath
Expand Down Expand Up @@ -127,10 +129,10 @@

bool OWNCLOUDSYNC_EXPORT isFolderReadOnly(const std::filesystem::path &path) noexcept;

/**
* Rename the file \a originFileName to \a destinationFileName, and
* overwrite the destination if it already exists - without extra checks.
*/

Check warning on line 135 in src/libsync/filesystem.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Edit this comment to use the C++ format, i.e. "//".

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFR-FY_QJQGNpnTv&open=AZ9vHFR-FY_QJQGNpnTv&pullRequest=10367
bool OWNCLOUDSYNC_EXPORT uncheckedRenameReplace(const QString &originFileName,
const QString &destinationFileName,
QString *errorString);
Expand Down
2 changes: 1 addition & 1 deletion src/libsync/syncengine.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2014 ownCloud GmbH
Expand Down Expand Up @@ -383,7 +383,7 @@
const auto lockOwnerTypeToSkipReadonly = _account->capabilities().filesLockTypeAvailable()
? SyncFileItem::LockOwnerType::TokenLock
: SyncFileItem::LockOwnerType::UserLock;
if (item->_locked == SyncFileItem::LockStatus::LockedItem

Check failure on line 386 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS0&open=AZ9vHFHpFY_QJQGNpnS0&pullRequest=10367
&& (item->_lockOwnerType != lockOwnerTypeToSkipReadonly || item->_lockOwnerId != account()->davUser())) {
qCDebug(lcEngine()) << filePath << "file is locked: making it read only";
FileSystem::setFileReadOnly(filePath, true);
Expand All @@ -397,7 +397,7 @@

// Update on-disk virtual file metadata
if (modificationHappened && item->_type == ItemTypeVirtualFile) {
auto r = _syncOptions._vfs->updateMetadata(*item, filePath, {});

Check warning on line 400 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "r" of type "class OCC::Result<enum OCC::Vfs::ConvertToPlaceholderResult, class QString>" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS1&open=AZ9vHFHpFY_QJQGNpnS1&pullRequest=10367
if (!r) {
item->_status = SyncFileItem::Status::NormalError;
item->_instruction = CSYNC_INSTRUCTION_ERROR;
Expand Down Expand Up @@ -501,15 +501,15 @@
const auto e2EeLockedFolders = _journal->e2EeLockedFolders();

if (!e2EeLockedFolders.isEmpty()) {
for (const auto &e2EeLockedFolder : e2EeLockedFolders) {

Check warning on line 504 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this declaration by a structured binding declaration.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS5&open=AZ9vHFHpFY_QJQGNpnS5&pullRequest=10367
const auto folderId = e2EeLockedFolder.first;

Check warning on line 505 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid this unnecessary copy by using a "const" reference.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS6&open=AZ9vHFHpFY_QJQGNpnS6&pullRequest=10367
qCInfo(lcEngine()) << "start unlock job for folderId:" << folderId;
const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), e2EeLockedFolder.second);
if (!folderToken) {

Check failure on line 508 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS4&open=AZ9vHFHpFY_QJQGNpnS4&pullRequest=10367
qCWarning(lcEngine()) << "decrypt failed";
return;
}
// TODO: We need to rollback changes done to metadata in case we have an active lock, this needs to be implemented on the server first

Check warning on line 512 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnSy&open=AZ9vHFHpFY_QJQGNpnSy&pullRequest=10367
const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, *folderToken, _journal, this);
unlockJob->setShouldRollbackMetadataChanges(true);
unlockJob->start();
Expand Down Expand Up @@ -638,7 +638,7 @@
_discoveryPhase->_account = _account;
_discoveryPhase->_excludes = _excludedFiles.data();
const QString excludeFilePath = _localPath + QStringLiteral(".sync-exclude.lst");
if (FileSystem::fileExists(excludeFilePath)) {

Check warning on line 641 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "excludeFilePath" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS3&open=AZ9vHFHpFY_QJQGNpnS3&pullRequest=10367
_discoveryPhase->_excludes->addExcludeFilePath(excludeFilePath);
_discoveryPhase->_excludes->reloadExcludeFiles();
}
Expand Down Expand Up @@ -700,7 +700,7 @@
connect(_discoveryPhase.get(), &DiscoveryPhase::itemDiscovered, this, &SyncEngine::slotItemDiscovered);
connect(_discoveryPhase.get(), &DiscoveryPhase::newBigFolder, this, &SyncEngine::newBigFolder);
connect(_discoveryPhase.get(), &DiscoveryPhase::existingFolderNowBig, this, &SyncEngine::existingFolderNowBig);
connect(_discoveryPhase.get(), &DiscoveryPhase::fatalError, this, [this](const QString &errorString, ErrorCategory errorCategory) {

Check warning on line 703 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "errorCategory" of type "enum OCC::ErrorCategory" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS7&open=AZ9vHFHpFY_QJQGNpnS7&pullRequest=10367
Q_EMIT syncError(errorString, errorCategory);
finalize(false);
});
Expand All @@ -721,7 +721,7 @@
const auto databaseFingerprint = _journal->dataFingerprint();
_discoveryPhase->_dataFingerprint = databaseFingerprint;
ProcessDirectoryJob::PathTuple path = {};
path._local = path._original = path._server = path._target = singleItemDiscoveryOptions().discoveryPath;

Check warning on line 724 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract the assignment to "_original" from this expression.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS8&open=AZ9vHFHpFY_QJQGNpnS8&pullRequest=10367

SyncJournalFileRecord rec;
const auto localQueryMode = _journal->getFileRecord(singleItemDiscoveryOptions().discoveryDirItem->_file, &rec) && rec.isValid()
Expand Down Expand Up @@ -871,8 +871,8 @@
item->_direction == SyncFileItem::Up && item->_status == SyncFileItem::Success;

if (isNewlyUploadedFile && item->_locked != SyncFileItem::LockStatus::LockedItem && _account->capabilities().filesLockAvailable() &&
(FileSystem::isMatchingOfficeFileExtension(item->_file) || FileSystem::isMatchingAutoCADDocumentExtension(item->_file))) {
(FileSystem::isMatchingOfficeFileExtension(item->_file) || FileSystem::isMatchingAutoCADDocumentExtension(item->_file) || FileSystem::isMatchingAdobeDocumentExtension(item->_file))) {
{

Check warning on line 875 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested code block into a separate function.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnS_&open=AZ9vHFHpFY_QJQGNpnS_&pullRequest=10367
SyncJournalFileRecord rec;
if (!_journal->getFileRecord(item->_file, &rec) || !rec.isValid()) {
qCWarning(lcEngine) << "Newly-created office file just uploaded but not in sync journal. Not going to lock it." << item->_file;
Expand Down Expand Up @@ -917,7 +917,7 @@
detectFileLock(item);
}

void SyncEngine::slotPropagationFinished(OCC::SyncFileItem::Status status)

Check warning on line 920 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "status" of type "enum OCC::SyncFileItem::Status" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTA&open=AZ9vHFHpFY_QJQGNpnTA&pullRequest=10367
{
if (_propagator->_anotherSyncNeeded && _anotherSyncNeeded == NoFollowUpSync) {
_anotherSyncNeeded = ImmediateFollowUp;
Expand Down Expand Up @@ -1032,7 +1032,7 @@
}
}

void SyncEngine::cancelSyncOrContinue(bool cancel)

Check warning on line 1035 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "cancel" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTB&open=AZ9vHFHpFY_QJQGNpnTB&pullRequest=10367
{
if (cancel) {
qCInfo(lcEngine) << "User aborted sync";
Expand Down Expand Up @@ -1144,7 +1144,7 @@
return false;
}

for (const auto &syncItem : _syncItems) {

Check warning on line 1147 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this range for-loop by "std::ranges::any_of".

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTC&open=AZ9vHFHpFY_QJQGNpnTC&pullRequest=10367
// If there's at least one remove instruction to be propagated to the remote: bail out, we might have lost a local rename.
if (syncItem->_instruction == CSYNC_INSTRUCTION_REMOVE && syncItem->_direction == SyncFileItem::Up) {
return true;
Expand All @@ -1153,7 +1153,7 @@
return false;
}

bool SyncEngine::handleMassDeletion()

Check failure on line 1156 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 33 to the 25 allowed.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTE&open=AZ9vHFHpFY_QJQGNpnTE&pullRequest=10367
{
const auto displayDialog = ConfigFile().promptDeleteFiles() && !_syncOptions.isCmd();
const auto allFilesDeleted = !_hasNoneFiles && _hasRemoveFile;
Expand All @@ -1163,11 +1163,11 @@
if (oneItem->_instruction == CSYNC_INSTRUCTION_REMOVE) {
if (oneItem->isDirectory()) {
const auto result = _journal->listFilesInPath(oneItem->_file.toUtf8(), [&deletionCounter] (const auto &oneRecord) {
if (oneRecord.isFile()) {

Check failure on line 1166 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTF&open=AZ9vHFHpFY_QJQGNpnTF&pullRequest=10367
++deletionCounter;
}
});
if (!result) {

Check failure on line 1170 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTG&open=AZ9vHFHpFY_QJQGNpnTG&pullRequest=10367
qCDebug(lcEngine()) << "unable to find the number of files within a deleted folder:" << oneItem->_file;
}
} else {
Expand All @@ -1177,7 +1177,7 @@
}
const auto filesDeletedThresholdExceeded = deletionCounter > ConfigFile().deleteFilesThreshold();

if ((allFilesDeleted || filesDeletedThresholdExceeded) && displayDialog) {

Check warning on line 1180 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "filesDeletedThresholdExceeded" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTD&open=AZ9vHFHpFY_QJQGNpnTD&pullRequest=10367
qCWarning(lcEngine) << "Many files are going to be deleted, asking the user";
int side = 0; // > 0 means more deleted on the server. < 0 means more deleted on the client
for (const auto &it : std::as_const(_syncItems)) {
Expand All @@ -1186,7 +1186,7 @@
}
}

promptUserBeforePropagation([this, side](auto &&callback){

Check failure on line 1189 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"std::forward" is never called on this forwarding reference argument.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTH&open=AZ9vHFHpFY_QJQGNpnTH&pullRequest=10367
emit aboutToRemoveAllFiles(side >= 0 ? SyncFileItem::Down : SyncFileItem::Up, callback);
});
return true;
Expand All @@ -1211,7 +1211,7 @@
slotAddTouchedFile(_localPath + oneFolder->_file);

if (oneFolder->_type == ItemType::ItemTypeDirectory) {
const auto deletionCallback = [this] (const QString &deleteItem, bool) {

Check warning on line 1214 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified unnamed variable of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTI&open=AZ9vHFHpFY_QJQGNpnTI&pullRequest=10367
slotAddTouchedFile(deleteItem);
};

Expand All @@ -1228,7 +1228,7 @@
{
QPointer<QObject> guard = new QObject();
QPointer<QObject> self = this;
auto callback = [this, self, guard](bool cancel) -> void {

Check warning on line 1231 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "cancel" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTK&open=AZ9vHFHpFY_QJQGNpnTK&pullRequest=10367

Check warning on line 1231 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the redundant return type of this lambda.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTJ&open=AZ9vHFHpFY_QJQGNpnTJ&pullRequest=10367
// use a guard to ensure its only called once...
// qpointer to self to ensure we still exist
if (!guard || !self) {
Expand Down Expand Up @@ -1278,7 +1278,7 @@
_leadingAndTrailingSpacesFilesAllowed.append(filePath);
}

void SyncEngine::setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value)

Check warning on line 1281 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "value" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTM&open=AZ9vHFHpFY_QJQGNpnTM&pullRequest=10367
{
_shouldEnforceWindowsFileNameCompatibility = value;
}
Expand Down Expand Up @@ -1315,14 +1315,14 @@
// This invariant is used in SyncEngine::shouldDiscoverLocally
QString prev;
auto it = _localDiscoveryPaths.begin();
while(it != _localDiscoveryPaths.end()) {
if (!prev.isNull() && it->startsWith(prev) && (prev.endsWith('/') || *it == prev || it->at(prev.size()) <= '/')) {
it = _localDiscoveryPaths.erase(it);
} else {
prev = *it;
++it;
}
}

Check warning on line 1325 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this loop with a "std::erase_if" call.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTN&open=AZ9vHFHpFY_QJQGNpnTN&pullRequest=10367
}

void SyncEngine::setSingleItemDiscoveryOptions(const SingleItemDiscoveryOptions &singleItemDiscoveryOptions)
Expand All @@ -1335,7 +1335,7 @@
return _singleItemDiscoveryOptions;
}

void SyncEngine::setFilesystemPermissionsReliable(bool reliable)

Check warning on line 1338 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTO&open=AZ9vHFHpFY_QJQGNpnTO&pullRequest=10367

Check warning on line 1338 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "reliable" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTP&open=AZ9vHFHpFY_QJQGNpnTP&pullRequest=10367
{
_filesystemPermissionsReliable = reliable;
}
Expand Down Expand Up @@ -1573,7 +1573,7 @@
it != _discoveryPhase->_filesNeedingScheduledSync.cend();
++it) {

const auto file = it.key();

Check warning on line 1576 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid this unnecessary copy by using a "const" reference.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9vHFHpFY_QJQGNpnTQ&open=AZ9vHFHpFY_QJQGNpnTQ&pullRequest=10367
const auto syncScheduledSecs = it.value();

// We don't want to schedule syncs again for files we have already discovered needing a
Expand Down
7 changes: 7 additions & 0 deletions sync-exclude.lst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
.~lock.*
~*.tmp

# Adobe lock files: InDesign/InCopy (.idlk) and Premiere Pro (.prlock)
# lock files. Unlike Office/LibreOffice lock files they do not encode the
# guarded document's extension, but excluding them by extension is safe as
# no legitimate user document uses it.
*.idlk
*.prlock

# AutoCAD lock files (.dwl / .dwl2) created when opening a .dwg drawing.
# Both share the document's base name and are deleted when it is closed.
*.dwl
Expand Down
12 changes: 12 additions & 0 deletions test/testexcludedfiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* any purpose.
*/

#include <QtTest>

Check failure on line 11 in test/testexcludedfiles.cpp

View workflow job for this annotation

GitHub Actions / build

test/testexcludedfiles.cpp:11:10 [clang-diagnostic-error]

'QtTest' file not found
#include <QTemporaryDir>

#include "csync_exclude.h"
Expand Down Expand Up @@ -182,6 +182,18 @@
QCOMPARE(check_file_full("my.~directory"), CSYNC_FILE_EXCLUDE_AND_REMOVE);
QCOMPARE(check_file_full("/a_folder/my.~directory"), CSYNC_FILE_EXCLUDE_AND_REMOVE);

/* Adobe lock files are excluded by extension (.idlk/.prlock) */
QCOMPARE(check_file_full("Test.idlk"), CSYNC_FILE_EXCLUDE_LIST);
QCOMPARE(check_file_full("~Test~0kjyv(.idlk"), CSYNC_FILE_EXCLUDE_LIST);
QCOMPARE(check_file_full("subdir/Test.idlk"), CSYNC_FILE_EXCLUDE_LIST);
QCOMPARE(check_file_full("Test.prlock"), CSYNC_FILE_EXCLUDE_LIST);
QCOMPARE(check_file_full("subdir/Test.prlock"), CSYNC_FILE_EXCLUDE_LIST);

/* Adobe documents themselves are NOT excluded */
QCOMPARE(check_file_full("Test.indd"), CSYNC_NOT_EXCLUDED);
QCOMPARE(check_file_full("Test.icml"), CSYNC_NOT_EXCLUDED);
QCOMPARE(check_file_full("Test.prproj"), CSYNC_NOT_EXCLUDED);

/* Not excluded because the pattern .netscape/cache requires directory. */
QCOMPARE(check_file_full(".netscape/cache"), CSYNC_NOT_EXCLUDED);

Expand Down
126 changes: 126 additions & 0 deletions test/testlockfile.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "lockfilejobs.h"

Check failure on line 6 in test/testlockfile.cpp

View workflow job for this annotation

GitHub Actions / build

test/testlockfile.cpp:6:10 [clang-diagnostic-error]

'lockfilejobs.h' file not found

#include "account.h"
#include "accountstate.h"
Expand Down Expand Up @@ -828,6 +828,33 @@
QCOMPARE(lockFileDetectedNewlyUploadedSpy.count(), 1);
}

void testLockFile_lockFile_detect_newly_uploaded_adobe_idlk()
{
// InDesign: opening a .indd creates `~{base}~{token}(.idlk`, which carries
// only the document's base name. The guarded document is resolved by
// matching a sibling .indd/.icml file in the same directory.
const auto testFileName = QStringLiteral("document.indd");
const auto testLockFileName = QStringLiteral("~document~0kjyv(.idlk");

const auto testDocumentsDirName = "documents";

FakeFolder fakeFolder{FileInfo{}};
fakeFolder.localModifier().mkdir(testDocumentsDirName);
// Adobe lock files are excluded from sync by the default exclude list;
// the FakeFolder does not load it, so replicate the exclusion here.
fakeFolder.syncEngine().excludedFiles().addManualExclude(QStringLiteral("*.idlk"));

fakeFolder.syncEngine().account()->setCapabilities({{"files", QVariantMap{{"locking", QByteArray{"1.0"}}}}});
QSignalSpy lockFileDetectedNewlyUploadedSpy(&fakeFolder.syncEngine(), &OCC::SyncEngine::lockFileDetected);

fakeFolder.localModifier().insert(testDocumentsDirName + QStringLiteral("/") + testLockFileName);
fakeFolder.localModifier().insert(testDocumentsDirName + QStringLiteral("/") + testFileName);

QVERIFY(fakeFolder.syncOnce());

QCOMPARE(lockFileDetectedNewlyUploadedSpy.count(), 1);
}

void testLockFile_autoCADLockFileTargetFilePath_resolution()
{
QTemporaryDir dir;
Expand Down Expand Up @@ -883,6 +910,105 @@
QVERIFY(orphan.path.isEmpty());
}

void testLockFile_lockFile_detect_newly_uploaded_adobe_prlock()
{
// Premiere Pro: opening a .prproj creates `{base}.prlock`.
const auto testFileName = QStringLiteral("project.prproj");
const auto testLockFileName = QStringLiteral("project.prlock");

const auto testDocumentsDirName = "documents";

FakeFolder fakeFolder{FileInfo{}};
fakeFolder.localModifier().mkdir(testDocumentsDirName);
fakeFolder.syncEngine().excludedFiles().addManualExclude(QStringLiteral("*.prlock"));

fakeFolder.syncEngine().account()->setCapabilities({{"files", QVariantMap{{"locking", QByteArray{"1.0"}}}}});
QSignalSpy lockFileDetectedNewlyUploadedSpy(&fakeFolder.syncEngine(), &OCC::SyncEngine::lockFileDetected);

fakeFolder.localModifier().insert(testDocumentsDirName + QStringLiteral("/") + testLockFileName);
fakeFolder.localModifier().insert(testDocumentsDirName + QStringLiteral("/") + testFileName);

QVERIFY(fakeFolder.syncOnce());

QCOMPARE(lockFileDetectedNewlyUploadedSpy.count(), 1);
}

void testLockFile_adobeLockFileTargetFilePath_resolution()
{
QTemporaryDir dir;
QVERIFY(dir.isValid());
const auto dirPath = dir.path() + QLatin1Char('/');

// InDesign: `~{base}~{token}(.idlk` guards `{base}.indd`.
const auto inddPath = dirPath + QStringLiteral("Test.indd");
const auto idlkPath = dirPath + QStringLiteral("~Test~0kjyv(.idlk");
QVERIFY(QFile{inddPath}.open(QIODevice::WriteOnly));
QVERIFY(QFile{idlkPath}.open(QIODevice::WriteOnly));

const auto idlkResolved = OCC::FileSystem::lockFileTargetFilePath(idlkPath, QString{});
QCOMPARE(idlkResolved.path, inddPath);
QCOMPARE(idlkResolved.type, OCC::FileSystem::FileLockingInfo::Type::Locked);

// Removing the lock file marks the document as unlocked (sibling still present).
QVERIFY(QFile::remove(idlkPath));
const auto idlkUnlocked = OCC::FileSystem::lockFileTargetFilePath(idlkPath, QString{});
QCOMPARE(idlkUnlocked.path, inddPath);
QCOMPARE(idlkUnlocked.type, OCC::FileSystem::FileLockingInfo::Type::Unlocked);

// Premiere Pro: `{base}.prlock` guards `{base}.prproj`.
const auto prprojPath = dirPath + QStringLiteral("project.prproj");
const auto prlockPath = dirPath + QStringLiteral("project.prlock");
QVERIFY(QFile{prprojPath}.open(QIODevice::WriteOnly));
QVERIFY(QFile{prlockPath}.open(QIODevice::WriteOnly));

const auto prlockResolved = OCC::FileSystem::lockFileTargetFilePath(prlockPath, QString{});
QCOMPARE(prlockResolved.path, prprojPath);
QCOMPARE(prlockResolved.type, OCC::FileSystem::FileLockingInfo::Type::Locked);

// InCopy story: `.idlk` may guard an `.icml` when no `.indd` sibling exists.
const auto icmlPath = dirPath + QStringLiteral("story.icml");
const auto idlk2Path = dirPath + QStringLiteral("~story~ab12cd(.idlk");
QVERIFY(QFile{icmlPath}.open(QIODevice::WriteOnly));
QVERIFY(QFile{idlk2Path}.open(QIODevice::WriteOnly));
const auto idlk2Resolved = OCC::FileSystem::lockFileTargetFilePath(idlk2Path, QString{});
QCOMPARE(idlk2Resolved.path, icmlPath);
QCOMPARE(idlk2Resolved.type, OCC::FileSystem::FileLockingInfo::Type::Locked);

// A non-lock file resolves to nothing.
const auto nonLock = OCC::FileSystem::lockFileTargetFilePath(dirPath + QStringLiteral("notes.txt"), QString{});
QVERIFY(nonLock.path.isEmpty());
QCOMPARE(nonLock.type, OCC::FileSystem::FileLockingInfo::Type::Unset);

// A lock file with no matching sibling document resolves to nothing.
const auto orphanIdlkPath = dirPath + QStringLiteral("~orphan~zz(.idlk");
QVERIFY(QFile{orphanIdlkPath}.open(QIODevice::WriteOnly));
const auto orphan = OCC::FileSystem::lockFileTargetFilePath(orphanIdlkPath, QString{});
QVERIFY(orphan.path.isEmpty());

// An .idlk file whose name does not match the InDesign `~base~token(.idlk`
// pattern resolves to nothing — the regex fails, base name is nullopt.
const QStringList malformedIdlkNames = {
QStringLiteral("Test.idlk"), // missing leading ~ and token
QStringLiteral("~Test.idlk"), // missing ~token(
QStringLiteral("~Test~token.idlk"), // missing trailing (
QStringLiteral("Test~token(.idlk"), // missing leading ~
};
for (const auto &malformedName : malformedIdlkNames) {
const auto malformedPath = dirPath + malformedName;
QVERIFY(QFile{malformedPath}.open(QIODevice::WriteOnly));
const auto resolved = OCC::FileSystem::lockFileTargetFilePath(malformedPath, QString{});
QVERIFY2(resolved.path.isEmpty(), qPrintable(QStringLiteral("Expected empty path for malformed idlk name: %1").arg(malformedName)));
QFile::remove(malformedPath);
}

// A non-Adobe, non-Office extension resolves to nothing.
const auto unknownExtPath = dirPath + QStringLiteral("file.unknown");
QVERIFY(QFile{unknownExtPath}.open(QIODevice::WriteOnly));
const auto unknownExt = OCC::FileSystem::lockFileTargetFilePath(unknownExtPath, QString{});
QVERIFY(unknownExt.path.isEmpty());
QCOMPARE(unknownExt.type, OCC::FileSystem::FileLockingInfo::Type::Unset);
}

void testLockFile_verifyE2eeFilesUseCorrectPath()
{
const auto e2eeRoot = QStringLiteral("encrypted");
Expand Down
Loading