Merge branch 'develop' into release/4.2.0

This commit is contained in:
J-Jamet
2025-09-12 16:14:19 +02:00
12 changed files with 206 additions and 104 deletions

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.merge
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB
@@ -32,9 +33,10 @@ import com.kunzisoft.keepass.database.element.node.NodeHandler
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.utils.readAllBytes
import java.io.IOException
import java.util.*
import java.util.UUID
class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
@@ -180,7 +182,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
}
/**
* Merge a KDB> database in a KDBX database,
* Merge a KDBX database in a KDBX database,
* Try to take into account the modification date of each element
* To make a merge as accurate as possible
*/
@@ -302,32 +304,113 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
}
// Manage deleted objects
databaseToMerge.deletedObjects.forEach { deletedObject ->
val deletedObjectId = deletedObject.uuid
val databaseEntry = database.getEntryById(deletedObjectId)
val databaseGroup = database.getGroupById(deletedObjectId)
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
val databaseIconModificationTime = databaseIcon?.lastModificationTime
if (databaseEntry != null
&& deletedObject.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
}
if (databaseGroup != null
&& deletedObject.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
}
if (databaseIcon != null
&& (
databaseIconModificationTime == null
|| (deletedObject.deletionTime.isAfter(databaseIconModificationTime))
)
) {
database.removeCustomIcon(deletedObjectId)
}
val deletedObjects = databaseToMerge.deletedObjects
deletedObjects.forEach { deletedObject ->
deleteEntry(deletedObject)
deleteGroup(deletedObject, deletedObjects)
deleteIcon(deletedObject)
// Attachments are removed and optimized during the database save
}
}
/**
* Delete an entry from the database with the [deletedEntry] id
*/
private fun deleteEntry(deletedEntry: DeletedObject) {
val databaseEntry = database.getEntryById(deletedEntry.uuid)
if (databaseEntry != null
&& deletedEntry.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
}
}
/**
* Check whether a node is in the list of deleted objects
*/
private fun Set<DeletedObject>.containsNode(node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>): Boolean {
return this.any { it.uuid == node.nodeId.id }
}
/**
* Check whether a node is not in the list of deleted objects
*/
private fun Set<DeletedObject>.notContainsNode(node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>): Boolean {
return !this.containsNode(node)
}
/**
* Get the first parent not deleted
*/
private fun firstNotDeletedParent(
node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>,
deletedObjects: Set<DeletedObject>
): GroupKDBX? {
var parent = node.parent
while (parent != null && deletedObjects.containsNode(parent)) {
parent = node.parent
}
return parent
}
/**
* Delete a group from the database with the [deletedGroup] id
* Recursively check whether a group to be deleted contains a node not to be deleted with [deletedObjects]
* and move it to the first parent that has not been deleted.
*/
private fun deleteGroup(deletedGroup: DeletedObject, deletedObjects: Set<DeletedObject>) {
val databaseGroup = database.getGroupById(deletedGroup.uuid)
if (databaseGroup != null
&& deletedGroup.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
// Must be in dedicated list to prevent modification collision
val entriesToMove = mutableListOf<EntryKDBX>()
databaseGroup.getChildEntries().forEach { child ->
// If the child entry is not a deleted object,
if (deletedObjects.notContainsNode(child)) {
entriesToMove.add(child)
}
}
val groupsToMove = mutableListOf<GroupKDBX>()
databaseGroup.getChildGroups().forEach { child ->
// Move the group to the first parent not deleted
// the deleted objects will take care of remove it later
groupsToMove.add(child)
}
// For each node to move, move it
// try to move the child entry in the first parent not deleted
entriesToMove.forEach { child ->
database.removeEntryFrom(child, child.parent)
database.addEntryTo(
child,
firstNotDeletedParent(databaseGroup, deletedObjects)
)
}
groupsToMove.forEach { child ->
database.removeGroupFrom(child, child.parent)
database.addGroupTo(
child,
firstNotDeletedParent(databaseGroup, deletedObjects)
)
}
// Then delete the group
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
}
}
/**
* Delete an icon from the database with the [deletedIcon] id
*/
private fun deleteIcon(deletedIcon: DeletedObject) {
val deletedObjectId = deletedIcon.uuid
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
val databaseIconModificationTime = databaseIcon?.lastModificationTime
if (databaseIcon != null
&& (databaseIconModificationTime == null
|| (deletedIcon.deletionTime.isAfter(databaseIconModificationTime)))
) {
database.removeCustomIcon(deletedObjectId)
}
}
/**
* Merge [customDataToMerge] in [customData]
*/

View File

@@ -154,7 +154,7 @@ class SearchHelper {
if (searchParameters.searchByDomain) {
try {
stringToCheck.inTheSameDomainAs(word, sameSubDomain = true)
} catch (e: Exception) {
} catch (_: Exception) {
false
}
} else null
@@ -220,10 +220,18 @@ class SearchHelper {
regex.matches(stringToCheck)
} else {
specialComparison?.invoke(stringToCheck, searchParameters.searchQuery)
?: stringToCheck.contains(
searchParameters.searchQuery,
!searchParameters.caseSensitive
)
?: run {
// Search with space separator #175
var searchFound = true
searchParameters.searchQuery.split(" ").forEach { word ->
searchFound = searchFound
&& stringToCheck.contains(
word,
!searchParameters.caseSensitive
)
}
searchFound
}
}
}
}

View File

@@ -119,19 +119,19 @@ fun <K : Parcelable, V : Parcelable> Parcel.writeParcelableMap(map: Map<K, V>, f
inline fun <reified K : Parcelable, reified V : Parcelable> Parcel.readParcelableMap(): Map<K, V> {
val size = readInt()
val map = HashMap<K, V>(size)
for (i in 0 until size) {
(0 until size).forEach { i ->
val key: K? = try {
when {
SDK_INT >= 33 -> readParcelable(K::class.java.classLoader, K::class.java)
else -> @Suppress("DEPRECATION") readParcelable(K::class.java.classLoader)
}
} catch (e: Exception) { null }
} catch (_: Exception) { null }
val value: V? = try {
when {
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
}
} catch (e: Exception) { null }
} catch (_: Exception) { null }
if (key != null && value != null)
map[key] = value
}
@@ -151,14 +151,14 @@ fun <V : Parcelable> Parcel.writeStringParcelableMap(map: HashMap<String, V>, fl
inline fun <reified V : Parcelable> Parcel.readStringParcelableMap(): LinkedHashMap<String, V> {
val size = readInt()
val map = LinkedHashMap<String, V>(size)
for (i in 0 until size) {
(0 until size).forEach { i ->
val key: String? = readString()
val value: V? = try {
when {
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
}
} catch (e: Exception) { null }
} catch (_: Exception) { null }
if (key != null && value != null)
map[key] = value
}
@@ -178,7 +178,7 @@ fun Parcel.writeStringIntMap(map: LinkedHashMap<String, Int>) {
fun Parcel.readStringIntMap(): LinkedHashMap<String, Int> {
val size = readInt()
val map = LinkedHashMap<String, Int>(size)
for (i in 0 until size) {
(0 until size).forEach { i ->
val key: String? = readString()
val value: Int = readInt()
if (key != null)
@@ -200,7 +200,7 @@ fun Parcel.writeStringStringMap(map: MutableMap<String, String>) {
fun Parcel.readStringStringMap(): LinkedHashMap<String, String> {
val size = readInt()
val map = LinkedHashMap<String, String>(size)
for (i in 0 until size) {
(0 until size).forEach { i ->
val key: String? = readString()
val value: String? = readString()
if (key != null && value != null)