Skip to content

Commit

Permalink
Add file/dir name normalization test
Browse files Browse the repository at this point in the history
Check that a file/directory name with NFC encoding on the server ends up
with the same encoding on the client, and that a subsequent
discovery+sync will not upload differently encoded files. Same for an
NFD encoded file/directory name.
  • Loading branch information
erikjv committed Feb 4, 2025
1 parent f638c9f commit 4e5d0ea
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 1 deletion.
77 changes: 77 additions & 0 deletions test/testlocaldiscovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,83 @@ private Q_SLOTS:
QVERIFY(!fakeFolder.currentRemoteState().find(QStringLiteral("C/.foo")));
QVERIFY(!fakeFolder.currentRemoteState().find(QStringLiteral("C/bar")));
}

void testNameNormalization_data()
{
QTest::addColumn<QString>("correct");
QTest::addColumn<QString>("incorrect");

const unsigned char a_umlaut_composed_bytes[] = {0xc3, 0xa4, 0x00};
const QString a_umlaut_composed = QString::fromUtf8(reinterpret_cast<const char *>(a_umlaut_composed_bytes));
const QString a_umlaut_decomposed = a_umlaut_composed.normalized(QString::NormalizationForm_D);

QTest::newRow("a_umlaut decomposed") << a_umlaut_decomposed << a_umlaut_composed;
QTest::newRow("a_umlaut composed") << a_umlaut_composed << a_umlaut_decomposed;
}

// Test that when a file/directory name on the remote is encoded in NFC, the local name is encoded
// in the same way, and that a subsequent sync does not change anything. And the same for NFD.
void testNameNormalization()
{
QFETCH_GLOBAL(Vfs::Mode, vfsMode);
QFETCH_GLOBAL(bool, filesAreDehydrated);

QFETCH(QString, correct);
QFETCH(QString, incorrect);

// Create an empty remote folder
FakeFolder fakeFolder({FileInfo{}}, vfsMode, filesAreDehydrated);
OperationCounter counter(fakeFolder);

// Create a file with an a-umlout in the "correct" normalization:
fakeFolder.remoteModifier().mkdir(QStringLiteral("P"));
fakeFolder.remoteModifier().mkdir(QStringLiteral("P/A"));
fakeFolder.remoteModifier().insert(QStringLiteral("P/A/") + correct);

// Same for a directory, holding a "normal" file:
fakeFolder.remoteModifier().mkdir(QStringLiteral("P/B") + correct);
fakeFolder.remoteModifier().insert(QStringLiteral("P/B") + correct + QStringLiteral("/b"));

LocalDiscoveryTracker tracker;
connect(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted, &tracker, &LocalDiscoveryTracker::slotItemCompleted);
connect(&fakeFolder.syncEngine(), &SyncEngine::finished, &tracker, &LocalDiscoveryTracker::slotSyncFinished);

// First sync: discover that there are files/directories on the server that are not yet synced to the local end
QVERIFY(fakeFolder.applyLocalModificationsAndSync());

// Check that locally we have the file and the directory with the correct names:
{
auto localState = fakeFolder.currentLocalState();
QVERIFY(localState.find(QStringLiteral("P/A/") + correct) != nullptr); // check if the file exists
QVERIFY(localState.find(QStringLiteral("P/B") + correct + QStringLiteral("/b")) != nullptr); // check if the file exists
}

counter.reset();

qDebug() << "*** MARK"; // Log marker to check if a PUT/DELETE shows up in the second sync

// Force a full local discovery on the next sync, which forces a walk of the (local) file system, reading back names (and file sizes/mtimes/etc.)...
fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, {QStringLiteral("P")});
tracker.startSyncFullDiscovery();

// ... and start the second sync:
QVERIFY(fakeFolder.applyLocalModificationsAndSync());

// If the normalization of the file/directory name did not change, no rename/move/etc. should have been detected, so check that the client didn't issue
// any of these operations:
QCOMPARE(counter.nDELETE, 0);
QCOMPARE(counter.nMOVE, 0);
QCOMPARE(counter.nPUT, 0);

// Check that the remote names are unchanged, and that no "incorrect" names have been introduced:
FileInfo &remoteState = fakeFolder.currentRemoteState();
QVERIFY(remoteState.find(QStringLiteral("P/A/") + correct) != nullptr); // check if the file still exists in the original normalization
QVERIFY(remoteState.find(QStringLiteral("P/A/") + incorrect) == nullptr); // there should NOT be a file with another normalization
QVERIFY(remoteState.find(QStringLiteral("P/B") + correct + QStringLiteral("/b"))
!= nullptr); // check if the directory still exists in the original normalization
QVERIFY(remoteState.find(QStringLiteral("P/B") + incorrect + QStringLiteral("/b"))
== nullptr); // there should NOT be a directory with another normalization
}
};

QTEST_GUILESS_MAIN(TestLocalDiscovery)
Expand Down
7 changes: 6 additions & 1 deletion test/testutils/syncenginetestutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -710,14 +710,19 @@ inline char *printDbData(const FileInfo &fi)
return QTest::toString(QStringLiteral("FileInfo with %1 files(%2)").arg(files.size()).arg(files.join(QStringLiteral(", "))));
}

/**
* @brief Utility class that count the number of GET/PUT/MOVE/DELETE operations during a sync.
*
* This can be used for subsequent syncs, but the counters need to be reset.
*/
struct OperationCounter
{
int nGET = 0;
int nPUT = 0;
int nMOVE = 0;
int nDELETE = 0;

OperationCounter() {};
OperationCounter() { }
OperationCounter(const OperationCounter &) = delete;
OperationCounter(OperationCounter &&) = delete;
void operator=(OperationCounter const &) = delete;
Expand Down

0 comments on commit 4e5d0ea

Please sign in to comment.