diff --git a/codecov.yml b/codecov.yml index d51ac47..5e1afa4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,3 +2,10 @@ ignore: - "kotpass/src/main/java/org/apache/commons/lang3/" - "kotpass/src/main/kotlin/org/redundent/kotlin/xml/" +coverage: + status: + project: + default: + target: 70% + threshold: 10% + informational: true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7013187..cb5896f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,24 +1,26 @@ [versions] -kotlin = "2.1.0" -dokka = "2.0.0" +kotlin = "2.1.21" +okio = "3.15.0" + +junit5 = "5.10.0" kotest = "5.6.1" -spotless = "6.25.0" -versions = "0.52.0" kover = "0.9.1" + +dokka = "2.0.0" maven-publish = "0.30.0" -junit5 = "5.10.0" +spotless = "6.25.0" +versions = "0.52.0" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } -versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } -spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } [libraries] kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } -testing-kotest = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } +okio = { module = "com.squareup.okio:okio", version.ref = "okio" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } - -okio = "com.squareup.okio:okio:3.10.2" +testing-kotest = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c35211..1b33c55 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72..d4081da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..db3a6ac 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/AutoTypeObfuscation.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/AutoTypeObfuscation.kt index 084a600..3cd23cf 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/AutoTypeObfuscation.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/AutoTypeObfuscation.kt @@ -1,6 +1,18 @@ package app.keemobile.kotpass.constants +/** + * Specifies the obfuscation method for Auto-Type to protect against keyloggers. + */ enum class AutoTypeObfuscation { + /** + * Sends characters as individual keystrokes. + * This is less secure against keyloggers. + */ None, + + /** + * Pastes the password via the system clipboard to bypass keyloggers. + * Also known as Two-Channel Auto-Type Obfuscation (TCATO). + */ UseClipboard } diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/CrsAlgorithm.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/CrsAlgorithm.kt index 7b52275..919dd35 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/CrsAlgorithm.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/CrsAlgorithm.kt @@ -1,8 +1,18 @@ package app.keemobile.kotpass.constants +/** + * Specifies the stream cipher for in-memory protection of sensitive data. + */ enum class CrsAlgorithm { + /** Unsupported. */ None, + + /** Legacy ArcFour (RC4) variant. Unsupported. */ ArcFourVariant, + + /** Salsa20 stream cipher. */ Salsa20, + + /** ChaCha20 stream cipher. */ ChaCha20 } diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/GroupOverride.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/GroupOverride.kt index 993c32a..ec7a0bd 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/GroupOverride.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/GroupOverride.kt @@ -1,5 +1,9 @@ package app.keemobile.kotpass.constants +/** + * Specifies whether a feature for a group is inherited from its parent + * or is explicitly enabled/disabled. + */ enum class GroupOverride { Inherit, Enabled, diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/HeaderFieldId.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/HeaderFieldId.kt index 9a9d6b7..8f4309f 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/HeaderFieldId.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/HeaderFieldId.kt @@ -1,19 +1,42 @@ package app.keemobile.kotpass.constants internal enum class HeaderFieldId { + /** Marks the end of the header fields section. */ EndOfHeader, - @Deprecated("No longer supported.") + /** Legacy comment field — deprecated and no longer used. */ Comment, + + /** Identifier for the encryption cipher used (e.g. AES, ChaCha20). */ CipherId, + + /** Compression algorithm identifier. */ Compression, + + /** Random seed used for master key derivation (32 bytes) */ MasterSeed, + + /** Random seed used for key transformation/derivation (legacy AES-KDF). */ TransformSeed, + + /** Number of transformation rounds for key derivation (legacy AES-KDF). */ TransformRounds, + + /** Initialization vector for the encryption cipher. */ EncryptionIV, + + /** Key used for the inner random stream encryption. */ InnerRandomStreamKey, + + /** First bytes of decrypted data used to verify correct decryption. */ StreamStartBytes, + + /** Identifier for inner random stream algorithm. */ InnerRandomStreamId, + + /** Parameters for modern key derivation functions stored as variant dictionary. */ KdfParameters, + + /** Custom data that can be read by third-party applications. */ PublicCustomData } diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/MemoryProtectionFlag.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/MemoryProtectionFlag.kt index df513c4..80cc18a 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/MemoryProtectionFlag.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/MemoryProtectionFlag.kt @@ -2,6 +2,9 @@ package app.keemobile.kotpass.constants import app.keemobile.kotpass.xml.FormatXml +/** + * Specifies which sensitive fields of an entry are protected in memory. + */ enum class MemoryProtectionFlag(val value: String) { Title(FormatXml.Tags.Meta.MemoryProtection.ProtectTitle), UserName(FormatXml.Tags.Meta.MemoryProtection.ProtectUserName), diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/Placeholder.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/Placeholder.kt index 422ac24..61220de 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/Placeholder.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/Placeholder.kt @@ -1,13 +1,31 @@ package app.keemobile.kotpass.constants +/** + * Represents dynamic placeholders used in KeePass. + */ internal enum class Placeholder(val value: String) { + /** The entry’s title: `{TITLE}`. */ Title("TITLE"), + + /** The entry’s user name: `{USERNAME}`. */ UserName("USERNAME"), + + /** The entry’s password: `{PASSWORD}`. */ Password("PASSWORD"), + + /** The entry’s URL: `{URL}`. */ Url("URL"), + + /** The entry’s notes: `{NOTES}`. */ Notes("NOTES"), + + /** The entry’s unique ID: `{UUID}`. */ Uuid("UUID"), + + /** Prefix for a field reference to another entry, e.g., `{REF:T@...}`. */ Reference("REF:"), + + /** Prefix for a custom string field, e.g., `{S:FieldName}`. */ CustomField("S:"); operator fun invoke() = this.value diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/EncryptionSaltGenerator.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/EncryptionSaltGenerator.kt index 28b81e6..e5b7daa 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/EncryptionSaltGenerator.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/EncryptionSaltGenerator.kt @@ -8,10 +8,6 @@ import app.keemobile.kotpass.extensions.sha256 import app.keemobile.kotpass.extensions.sha512 import okio.ByteString -private val SalsaNonce = intArrayOf(0xe8, 0x30, 0x09, 0x4b, 0x97, 0x20, 0x5d, 0x2a) - .map(Int::toByte) - .toByteArray() - /** * Used as inner encryption to improve process memory protection, it does not enhance * the cryptographic security of the KDBX file format itself. @@ -39,8 +35,11 @@ sealed class EncryptionSaltGenerator { abstract fun processBytes(input: ByteArray): ByteArray class Salsa20(key: ByteArray) : EncryptionSaltGenerator() { + // Static 'nonce' provided by KeePass specification + private val nonce = byteArrayOf(0xe8.toByte(), 0x30, 0x09, 0x4b, 0x97.toByte(), 0x20, 0x5d, 0x2a) + private val engine = Salsa20Engine().apply { - init(key.sha256(), SalsaNonce) + init(key.sha256(), nonce) } override fun getSalt(length: Int) = engine.getBytes(length) diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/Signature.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/Signature.kt index d9d1a43..b681c1a 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/Signature.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/Signature.kt @@ -5,10 +5,10 @@ import app.keemobile.kotpass.io.BufferedStream import okio.BufferedSink import okio.ByteString -class Signature( - val base: ByteString, - val secondary: ByteString -) { +/** + * This signature is used to identify the file type and version. + */ +class Signature(val base: ByteString, val secondary: ByteString) { internal fun writeTo(sink: BufferedSink) = with(sink) { write(base) write(secondary) diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantItem.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantItem.kt index bb43c92..5376d58 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantItem.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantItem.kt @@ -3,41 +3,62 @@ package app.keemobile.kotpass.database.header import app.keemobile.kotpass.constants.VariantTypeId import okio.ByteString +/** + * Represents a variant data type item within KDBX file header. + */ sealed interface VariantItem { + /** + * The unique identifier for the variant's data type, + * corresponding to [VariantTypeId]. + */ val typeId: Int + /** Represents an unsigned 32-bit integer variant item. */ @JvmInline value class UInt32(val value: UInt) : VariantItem { - override val typeId: Int get() = VariantTypeId.UInt32 + override val typeId: Int + get() = VariantTypeId.UInt32 } + /** Represents an unsigned 64-bit integer variant item. */ @JvmInline value class UInt64(val value: ULong) : VariantItem { - override val typeId: Int get() = VariantTypeId.UInt64 + override val typeId: Int + get() = VariantTypeId.UInt64 } + /** Represents a boolean variant item. */ @JvmInline value class Bool(val value: Boolean) : VariantItem { - override val typeId: Int get() = VariantTypeId.Bool + override val typeId: Int + get() = VariantTypeId.Bool } + /** Represents a signed 32-bit integer variant item. */ @JvmInline value class Int32(val value: Int) : VariantItem { - override val typeId: Int get() = VariantTypeId.Int32 + override val typeId: Int + get() = VariantTypeId.Int32 } + /** Represents a signed 64-bit integer variant item. */ @JvmInline value class Int64(val value: Long) : VariantItem { - override val typeId: Int get() = VariantTypeId.Int64 + override val typeId: Int + get() = VariantTypeId.Int64 } + /** Represents a UTF-8 encoded string variant item. */ @JvmInline value class StringUtf8(val value: String) : VariantItem { - override val typeId: Int get() = VariantTypeId.StringUtf8 + override val typeId: Int + get() = VariantTypeId.StringUtf8 } + /** Represents a byte array variant item. */ @JvmInline value class Bytes(val value: ByteString) : VariantItem { - override val typeId: Int get() = VariantTypeId.Bytes + override val typeId: Int + get() = VariantTypeId.Bytes } } diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Binaries.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Binaries.kt index 73ebf07..ae1c61a 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Binaries.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Binaries.kt @@ -7,12 +7,20 @@ import app.keemobile.kotpass.models.Group import okio.ByteString import java.util.Stack +/** + * Returns a map of binary data associated with [KeePassDatabase]. + */ val KeePassDatabase.binaries get() = when (this) { is KeePassDatabase.Ver3x -> content.meta.binaries is KeePassDatabase.Ver4x -> innerHeader.binaries } +/** + * Modifies binaries map of [KeePassDatabase] using the provided [block]. + * + * @return A new [KeePassDatabase] instance with the modified binaries. + */ inline fun KeePassDatabase.modifyBinaries( crossinline block: (Map) -> Map ): KeePassDatabase = when (this) { @@ -26,6 +34,12 @@ inline fun KeePassDatabase.modifyBinaries( ) } +/** + * This function traverses all groups and entries, including historical entries, + * to identify and remove unreferenced binary data. + * + * @return A new [KeePassDatabase] instance with unused binaries removed. + */ fun KeePassDatabase.removeUnusedBinaries(): KeePassDatabase { val cleanupList = binaries.keys.toMutableSet() diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Entry.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Entry.kt index 430ebf8..0a009ea 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Entry.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Entry.kt @@ -9,6 +9,13 @@ import app.keemobile.kotpass.models.TimeData import java.time.Instant import java.util.UUID +/** + * Moves an entry to a new parent group. + * + * @param uuid The UUID of the entry to move. + * @param parentGroup The UUID of the destination parent group. + * @return A new [KeePassDatabase] instance with the entry moved. + */ fun KeePassDatabase.moveEntry( uuid: UUID, parentGroup: UUID @@ -29,6 +36,13 @@ fun KeePassDatabase.moveEntry( } } +/** + * Modifies a specific entry in the database. + * + * @param uuid The UUID of the entry to modify. + * @param block A lambda that takes [Entry] as a receiver and returns modified [Entry]. + * @return A new [KeePassDatabase] instance with the entry modified. + */ fun KeePassDatabase.modifyEntry( uuid: UUID, block: Entry.() -> Entry @@ -36,12 +50,24 @@ fun KeePassDatabase.modifyEntry( copy(group = group.modifyEntry(uuid, block)) } +/** + * Modifies all entries in the database. + * + * @param block A lambda that takes [Entry] as a receiver and returns modified [Entry]. + * @return A new [KeePassDatabase] instance with all entries modified. + */ fun KeePassDatabase.modifyEntries( block: Entry.() -> Entry ) = modifyContent { copy(group = group.modifyEntries(block)) } +/** + * Removes an entry from the database and adds it to the deleted objects list. + * + * @param uuid The UUID of the entry to remove. + * @return A new [KeePassDatabase] instance with the entry removed. + */ fun KeePassDatabase.removeEntry( uuid: UUID ) = modifyContent { @@ -51,6 +77,12 @@ fun KeePassDatabase.removeEntry( ) } +/** + * Creates a new entry with a historical record of the current entry. + * + * @param block A lambda that takes [Entry] as a receiver and returns modified [Entry]. + * @return A new [Entry] instance with the current entry added to its history. + */ fun Entry.withHistory( block: Entry.() -> Entry ): Entry { @@ -60,6 +92,13 @@ fun Entry.withHistory( ) } +/** + * Modifies a specific entry within this group or its subgroups. + * + * @param uuid The UUID of the entry to modify. + * @param block A lambda that takes [Entry] as a receiver and returns modified [Entry]. + * @return A new [Group] instance with the entry modified. + */ private fun Group.modifyEntry( uuid: UUID, block: Entry.() -> Entry @@ -80,6 +119,12 @@ private fun Group.modifyEntry( } } +/** + * Modifies all entries within this group and its subgroups. + * + * @param block A lambda that takes [Entry] as a receiver and returns modified [Entry]. + * @return A new [Group] instance with all entries modified. + */ private fun Group.modifyEntries( block: Entry.() -> Entry ): Group = copy( @@ -101,6 +146,12 @@ private fun Group.modifyEntries( groups = groups.map { it.modifyEntries(block) } ) +/** + * Removes an entry from this group or its subgroups. + * + * @param uuid The UUID of the entry to remove. + * @return A new [Group] instance with the entry removed. + */ private fun Group.removeChildEntry( uuid: UUID ): Group { diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Group.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Group.kt index ab28526..cfa92e6 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Group.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/modifiers/Group.kt @@ -10,6 +10,13 @@ import java.time.Instant import java.util.Stack import java.util.UUID +/** + * Moves a group within [KeePassDatabase] to a new parent group. + * + * @param uuid The [UUID] of the group to be moved. + * @param parentGroup The [UUID] of the target parent group where the group will be moved. + * @return A new [KeePassDatabase] instance with the group moved. + */ fun KeePassDatabase.moveGroup( uuid: UUID, parentGroup: UUID @@ -33,12 +40,25 @@ fun KeePassDatabase.moveGroup( } } +/** + * Modifies the immediate children of the root group in [KeePassDatabase]. + * + * @param block A lambda that transforms [Group] instance of the root group’s children. + * @return A new [KeePassDatabase] instance with the modified root group children. + */ fun KeePassDatabase.modifyParentGroup( block: Group.() -> Group ) = modifyContent { copy(group = group.modifyGroup(group.uuid, block)) } +/** + * Modifies a specific group within [KeePassDatabase] identified by its [UUID]. + * + * @param uuid The [UUID] of the group to be modified. + * @param block A lambda that transforms the found [Group] instance. + * @return A new [KeePassDatabase] instance with the modified group. + */ fun KeePassDatabase.modifyGroup( uuid: UUID, block: Group.() -> Group @@ -46,12 +66,24 @@ fun KeePassDatabase.modifyGroup( copy(group = group.modifyGroup(uuid, block)) } +/** + * Applies a modification block to all groups within [KeePassDatabase]. + * + * @param block A lambda that transforms each [Group] instance. + * @return A new [KeePassDatabase] instance with all groups potentially modified. + */ fun KeePassDatabase.modifyGroups( block: Group.() -> Group ) = modifyContent { copy(group = group.modifyGroups(block)) } +/** + * Removes a group and all its nested children and entries from [KeePassDatabase]. + * + * @param uuid The [UUID] of the group to be removed. + * @return A new [KeePassDatabase] instance with the group and its contents removed. + */ fun KeePassDatabase.removeGroup( uuid: UUID ): KeePassDatabase { @@ -66,6 +98,13 @@ fun KeePassDatabase.removeGroup( } } +/** + * Finds all UUIDs of a given group and its direct and indirect + * children (both groups and entries). + * + * @param uuid The [UUID] of the group. + * @return A [List] of [UUID]s. + */ private fun KeePassDatabase.findGroupChildIds( uuid: UUID ): List { @@ -88,6 +127,12 @@ private fun KeePassDatabase.findGroupChildIds( return uuids } +/** + * Removes a child group from the current group’s hierarchy. + * + * @param uuid The [UUID] of the child group to be removed. + * @return A new [Group] instance with the specified child group removed. + */ private fun Group.removeChildGroup( uuid: UUID ): Group { @@ -98,6 +143,13 @@ private fun Group.removeChildGroup( } } +/** + * Modifies a specific group within the current group’s hierarchy. + * + * @param uuid The [UUID] of the group to be modified. + * @param block A lambda that transforms the found [Group] instance. + * @return A new [Group] instance with the specified group modified. + */ private fun Group.modifyGroup( uuid: UUID, block: Group.() -> Group @@ -115,6 +167,12 @@ private fun Group.modifyGroup( } } +/** + * Applies a modification block to all groups within the current group’s hierarchy. + * + * @param block A lambda that transforms each [Group] instance. + * @return A new [Group] instance with all groups potentially modified. + */ private fun Group.modifyGroups( block: Group.() -> Group ): Group { diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/xml/Instant.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/xml/Instant.kt index 5d263c7..03ead64 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/xml/Instant.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/xml/Instant.kt @@ -8,6 +8,7 @@ import app.keemobile.kotpass.io.encodeBase64 import app.keemobile.kotpass.models.XmlContext import org.redundent.kotlin.xml.Node import java.time.Instant +import java.time.format.DateTimeFormatter private const val EpochSecondsFromAD = 62135596800 @@ -27,6 +28,6 @@ internal fun Instant.marshal(context: XmlContext.Encode): String { return if (binary) { (epochSecond + EpochSecondsFromAD).toByteArray().encodeBase64() } else { - this.toString() + DateTimeFormatter.ISO_INSTANT.format(this) } } diff --git a/kotpass/src/main/kotlin/org/redundent/kotlin/xml/Node.kt b/kotpass/src/main/kotlin/org/redundent/kotlin/xml/Node.kt index d0cf5b7..9a5ea75 100644 --- a/kotpass/src/main/kotlin/org/redundent/kotlin/xml/Node.kt +++ b/kotpass/src/main/kotlin/org/redundent/kotlin/xml/Node.kt @@ -256,7 +256,9 @@ open class Node(val nodeName: String) : Element { } if (_globalLevelProcessingInstructions.isNotEmpty()) { - _globalLevelProcessingInstructions.forEach { it.render(appendable, "", printOptions) } + for (element in _globalLevelProcessingInstructions) { + element.render(appendable, "", printOptions) + } } render(appendable, "", printOptions) diff --git a/kotpass/src/main/kotlin/org/redundent/kotlin/xml/XmlBuilder.kt b/kotpass/src/main/kotlin/org/redundent/kotlin/xml/XmlBuilder.kt index d318b38..309dd22 100644 --- a/kotpass/src/main/kotlin/org/redundent/kotlin/xml/XmlBuilder.kt +++ b/kotpass/src/main/kotlin/org/redundent/kotlin/xml/XmlBuilder.kt @@ -88,19 +88,14 @@ fun parse(document: Document): Node { copyAttributes(root, result) val children = root.childNodes - (0 until children.length) - .map(children::item) - .forEach { copy(it, result) } - + for (i in 0.. { dest.cdata(source.nodeValue) } - W3CNode.TEXT_NODE -> { dest.text(source.nodeValue.trim { it.isWhitespace() || it == '\r' || it == '\n' }) } @@ -132,13 +126,12 @@ private fun copyAttributes(source: W3CNode, dest: Node) { return } - (0 until attributes.length) - .map(attributes::item) - .forEach { - if (it.nodeName.startsWith("xmlns")) { - dest.namespace(it.nodeName.substring(min(6, it.nodeName.length)), it.nodeValue) - } else { - dest.attribute(it.nodeName, it.nodeValue) - } + for (i in 0..