mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
Compare commits
3 Commits
5332075193
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd22010288 | ||
|
|
df4de58541 | ||
|
|
a9e0de34d1 |
@@ -211,6 +211,78 @@ namespace
|
||||
return entry.take();
|
||||
}
|
||||
|
||||
/*!
|
||||
* Create nested folder hierarchy from a path string.
|
||||
* For example, "Socials/Forums" creates a "Socials" group with a "Forums" child group.
|
||||
* Returns the deepest (leaf) group in the hierarchy.
|
||||
*/
|
||||
Group*
|
||||
createNestedFolderHierarchy(const QString& folderPath, Group* rootGroup, QMap<QString, Group*>& createdGroups)
|
||||
{
|
||||
if (folderPath.isEmpty()) {
|
||||
return rootGroup;
|
||||
}
|
||||
|
||||
// Check if we've already created this exact path
|
||||
if (createdGroups.contains(folderPath)) {
|
||||
return createdGroups.value(folderPath);
|
||||
}
|
||||
|
||||
// Split the path by forward slashes
|
||||
QStringList pathParts = folderPath.split('/', Qt::SkipEmptyParts);
|
||||
if (pathParts.isEmpty()) {
|
||||
return rootGroup;
|
||||
}
|
||||
|
||||
Group* currentParent = rootGroup;
|
||||
QString currentPath;
|
||||
|
||||
// Create each level of the hierarchy
|
||||
for (int i = 0; i < pathParts.size(); ++i) {
|
||||
const QString& partName = pathParts[i];
|
||||
|
||||
// Build the current path (e.g., "Socials", then "Socials/Forums")
|
||||
if (currentPath.isEmpty()) {
|
||||
currentPath = partName;
|
||||
} else {
|
||||
currentPath += "/" + partName;
|
||||
}
|
||||
|
||||
// Check if this level already exists
|
||||
Group* existingGroup = createdGroups.value(currentPath);
|
||||
if (existingGroup) {
|
||||
currentParent = existingGroup;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find existing child group with this name
|
||||
existingGroup = nullptr;
|
||||
for (Group* child : currentParent->children()) {
|
||||
if (child->name() == partName) {
|
||||
existingGroup = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingGroup) {
|
||||
// Use existing group
|
||||
createdGroups.insert(currentPath, existingGroup);
|
||||
currentParent = existingGroup;
|
||||
} else {
|
||||
// Create new group
|
||||
auto newGroup = new Group();
|
||||
newGroup->setUuid(QUuid::createUuid());
|
||||
newGroup->setName(partName);
|
||||
newGroup->setParent(currentParent);
|
||||
|
||||
createdGroups.insert(currentPath, newGroup);
|
||||
currentParent = newGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return currentParent;
|
||||
}
|
||||
|
||||
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db)
|
||||
{
|
||||
auto folderField = QString("folders");
|
||||
@@ -224,15 +296,19 @@ namespace
|
||||
return;
|
||||
}
|
||||
|
||||
// Create groups from folders and store a temporary map of id -> uuid
|
||||
// Create groups from folders and store a temporary map of id -> group
|
||||
QMap<QString, Group*> folderMap;
|
||||
for (const auto& folder : vault.value(folderField).toArray()) {
|
||||
auto group = new Group();
|
||||
group->setUuid(QUuid::createUuid());
|
||||
group->setName(folder.toObject().value("name").toString());
|
||||
group->setParent(db->rootGroup());
|
||||
QMap<QString, Group*> createdGroups; // Track created groups by path to avoid duplicates
|
||||
|
||||
folderMap.insert(folder.toObject().value("id").toString(), group);
|
||||
for (const auto& folder : vault.value(folderField).toArray()) {
|
||||
const QString folderName = folder.toObject().value("name").toString();
|
||||
const QString folderId = folder.toObject().value("id").toString();
|
||||
|
||||
// Create the nested folder hierarchy
|
||||
Group* targetGroup = createNestedFolderHierarchy(folderName, db->rootGroup(), createdGroups);
|
||||
|
||||
// Map the folder ID to the target group
|
||||
folderMap.insert(folderId, targetGroup);
|
||||
}
|
||||
|
||||
QString folderId;
|
||||
|
||||
@@ -317,6 +317,56 @@ void TestImports::testBitwardenPasskey()
|
||||
QStringLiteral("aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw"));
|
||||
}
|
||||
|
||||
void TestImports::testBitwardenNestedFolders()
|
||||
{
|
||||
auto bitwardenPath =
|
||||
QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_nested_export.json"));
|
||||
|
||||
BitwardenReader reader;
|
||||
auto db = reader.convert(bitwardenPath);
|
||||
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
|
||||
QVERIFY(db);
|
||||
|
||||
// Test nested folder structure: "Socials/Forums" should create Socials -> Forums hierarchy
|
||||
auto entry = db->rootGroup()->findEntryByPath("/Socials/Forums/Reddit Account");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->title(), QStringLiteral("Reddit Account"));
|
||||
QCOMPARE(entry->username(), QStringLiteral("myuser"));
|
||||
|
||||
// Test deeper nesting: "Work/Projects/Client A"
|
||||
entry = db->rootGroup()->findEntryByPath("/Work/Projects/Client A/Client Portal");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->title(), QStringLiteral("Client Portal"));
|
||||
QCOMPARE(entry->username(), QStringLiteral("clientuser"));
|
||||
|
||||
// Test simple folder (no nesting): "Personal"
|
||||
entry = db->rootGroup()->findEntryByPath("/Personal/Personal Email");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->title(), QStringLiteral("Personal Email"));
|
||||
QCOMPARE(entry->username(), QStringLiteral("personal@email.com"));
|
||||
|
||||
// Verify the folder hierarchy exists
|
||||
auto socialsGroup = db->rootGroup()->findGroupByPath("/Socials");
|
||||
QVERIFY(socialsGroup);
|
||||
QCOMPARE(socialsGroup->name(), QStringLiteral("Socials"));
|
||||
|
||||
auto forumsGroup = socialsGroup->findGroupByPath("Forums");
|
||||
QVERIFY(forumsGroup);
|
||||
QCOMPARE(forumsGroup->name(), QStringLiteral("Forums"));
|
||||
|
||||
auto workGroup = db->rootGroup()->findGroupByPath("/Work");
|
||||
QVERIFY(workGroup);
|
||||
QCOMPARE(workGroup->name(), QStringLiteral("Work"));
|
||||
|
||||
auto projectsGroup = workGroup->findGroupByPath("Projects");
|
||||
QVERIFY(projectsGroup);
|
||||
QCOMPARE(projectsGroup->name(), QStringLiteral("Projects"));
|
||||
|
||||
auto clientAGroup = projectsGroup->findGroupByPath("Client A");
|
||||
QVERIFY(clientAGroup);
|
||||
QCOMPARE(clientAGroup->name(), QStringLiteral("Client A"));
|
||||
}
|
||||
|
||||
void TestImports::testProtonPass()
|
||||
{
|
||||
auto protonPassPath =
|
||||
|
||||
@@ -31,6 +31,7 @@ private slots:
|
||||
void testBitwarden();
|
||||
void testBitwardenEncrypted();
|
||||
void testBitwardenPasskey();
|
||||
void testBitwardenNestedFolders();
|
||||
void testProtonPass();
|
||||
};
|
||||
|
||||
|
||||
72
tests/data/bitwarden_nested_export.json
Normal file
72
tests/data/bitwarden_nested_export.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"name": "Socials/Forums"
|
||||
},
|
||||
{
|
||||
"id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
|
||||
"name": "Work/Projects/Client A"
|
||||
},
|
||||
{
|
||||
"id": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
|
||||
"name": "Personal"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa",
|
||||
"organizationId": null,
|
||||
"folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"type": 1,
|
||||
"name": "Reddit Account",
|
||||
"notes": "My reddit login",
|
||||
"favorite": false,
|
||||
"login": {
|
||||
"username": "myuser",
|
||||
"password": "mypass",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "https://reddit.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||
"organizationId": null,
|
||||
"folderId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
|
||||
"type": 1,
|
||||
"name": "Client Portal",
|
||||
"notes": "Client A portal login",
|
||||
"favorite": false,
|
||||
"login": {
|
||||
"username": "clientuser",
|
||||
"password": "clientpass",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "https://clienta.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||
"organizationId": null,
|
||||
"folderId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
|
||||
"type": 1,
|
||||
"name": "Personal Email",
|
||||
"notes": "My personal email",
|
||||
"favorite": false,
|
||||
"login": {
|
||||
"username": "personal@email.com",
|
||||
"password": "personalpass",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "https://mail.provider.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user