diff --git a/gradle.properties b/gradle.properties index 28c5fc77c22cb5dd8b01c89c90a2ed08e1eca25a..5b97f2a777eb147bd02e23a888d485ca4d961cbb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,7 @@ org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m kotlin.code.style=official + +bouncyCastleVersion=1.68 hamcrestVersion=2.1 junit5Version=5.6.0 kotlinBitflagsVersion=1.1.0 diff --git a/libquassel-protocol/build.gradle.kts b/libquassel-protocol/build.gradle.kts index 4ce962d86f96cfe2b698d4e1c3748890460b1a9e..530c483a520e1e252f534525b261fbf4480109d6 100644 --- a/libquassel-protocol/build.gradle.kts +++ b/libquassel-protocol/build.gradle.kts @@ -17,6 +17,8 @@ dependencies { api("org.threeten", "threetenbp", "1.4.0") val kotlinBitflagsVersion: String by project api("de.justjanne", "kotlin-bitflags", kotlinBitflagsVersion) + val bouncyCastleVersion: String by project + implementation("org.bouncycastle", "bcpkix-jdk15on", bouncyCastleVersion) api(project(":libquassel-annotations")) ksp(project(":libquassel-generator")) } diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/ConnectedClient.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/ConnectedClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..d4eae08744666ccaa463dc5641e37426f299ec26 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/ConnectedClient.kt @@ -0,0 +1,66 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.models + +import de.justjanne.bitflags.of +import de.justjanne.bitflags.toBits +import de.justjanne.libquassel.protocol.features.FeatureSet +import de.justjanne.libquassel.protocol.features.LegacyFeature +import de.justjanne.libquassel.protocol.features.QuasselFeatureName +import de.justjanne.libquassel.protocol.models.types.QtType +import de.justjanne.libquassel.protocol.variant.QVariantMap +import de.justjanne.libquassel.protocol.variant.into +import de.justjanne.libquassel.protocol.variant.qVariant +import org.threeten.bp.Instant + +data class ConnectedClient( + val id: Int, + val remoteAddress: String, + val location: String, + val version: String, + val versionDate: Instant?, + val connectedSince: Instant, + val secure: Boolean, + val features: FeatureSet +) { + fun toVariantMap() = mapOf( + "id" to qVariant(id, QtType.Int), + "remoteAddress" to qVariant(remoteAddress, QtType.QString), + "location" to qVariant(location, QtType.QString), + "clientVersion" to qVariant(version, QtType.QString), + "clientVersionDate" to qVariant(versionDate?.epochSecond?.toString(), QtType.QString), + "connectedSince" to qVariant(connectedSince, QtType.QDateTime), + "secure" to qVariant(secure, QtType.Bool), + "features" to qVariant(features.legacyFeatures().toBits(), QtType.UInt), + "featureList" to qVariant(features.featureList().map(QuasselFeatureName::name), QtType.QStringList) + ) + + companion object { + fun fromVariantMap(properties: QVariantMap) = ConnectedClient( + id = properties["id"].into(-1), + remoteAddress = properties["remoteAddress"].into(""), + location = properties["location"].into(""), + version = properties["clientVersion"].into(""), + versionDate = properties["clientVersionDate"].into("") + .toLongOrNull() + ?.let(Instant::ofEpochSecond), + connectedSince = properties["connectedSince"].into(Instant.EPOCH), + secure = properties["secure"].into(false), + features = FeatureSet.build( + LegacyFeature.of(properties["features"].into()), + properties["featureList"].into<QStringList>() + ?.filterNotNull() + ?.map(::QuasselFeatureName) + .orEmpty() + ) + ) + } +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightNickType.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightNickType.kt index 4d6d996c1f21e7c40687f02902f1135bb0657186..8cf451b25fb7c677d09b5a2f0361033894a3ed58 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightNickType.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightNickType.kt @@ -11,11 +11,11 @@ package de.justjanne.libquassel.protocol.models enum class HighlightNickType( - val value: UByte, + val value: Int, ) { - NoNick(0x00u), - CurrentNick(0x01u), - AllNicks(0x02u); + NoNick(0), + CurrentNick(1), + AllNicks(2); companion object { private val values = enumValues<HighlightNickType>() @@ -24,6 +24,6 @@ enum class HighlightNickType( /** * Obtain from underlying representation */ - fun of(value: UByte): HighlightNickType? = values[value] + fun of(value: Int): HighlightNickType? = values[value] } } diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightRule.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightRule.kt new file mode 100644 index 0000000000000000000000000000000000000000..af45a7162c077b8902bba5f4dfe8710e80cf55df --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightRule.kt @@ -0,0 +1,43 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.models + +import de.justjanne.libquassel.protocol.util.expression.ExpressionMatch + +data class HighlightRule( + val id: Int, + val contents: String, + val isRegEx: Boolean = false, + val isCaseSensitive: Boolean = false, + val isEnabled: Boolean = true, + val isInverse: Boolean = false, + val sender: String, + val channel: String +) { + val contentMatch = ExpressionMatch( + contents, + if (isRegEx) ExpressionMatch.MatchMode.MatchRegEx + else ExpressionMatch.MatchMode.MatchPhrase, + isCaseSensitive + ) + val senderMatch = ExpressionMatch( + sender, + if (isRegEx) ExpressionMatch.MatchMode.MatchRegEx + else ExpressionMatch.MatchMode.MatchMultiWildcard, + isCaseSensitive + ) + val channelMatch = ExpressionMatch( + channel, + if (isRegEx) ExpressionMatch.MatchMode.MatchRegEx + else ExpressionMatch.MatchMode.MatchMultiWildcard, + isCaseSensitive + ) +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt index d392cb32528ab099f2cfbfa4f0865fb16f13f337..95fd8c0edc8d4590ae6e38285fc4a1d17302c110 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt @@ -22,28 +22,25 @@ import de.justjanne.libquassel.protocol.variant.QVariantMap import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.qVariant import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class AliasManager constructor( session: Session ) : SyncableObject(session, "AliasManager"), AliasManagerStub { override fun toVariantMap(): QVariantMap = mapOf( - "Aliases" to qVariant(initAliases(), QtType.QVariantMap) + "Aliases" to qVariant( + mapOf( + "names" to qVariant(aliases().map(Alias::name), QtType.QStringList), + "expansions" to qVariant(aliases().map(Alias::expansion), QtType.QStringList) + ), + QtType.QVariantMap + ) ) override fun fromVariantMap(properties: QVariantMap) { - initSetAliases(properties["Aliases"].into<QVariantMap>().orEmpty()) - } - - private fun initAliases(): QVariantMap = mapOf( - "names" to qVariant(aliases().map(Alias::name), QtType.QStringList), - "expansions" to qVariant(aliases().map(Alias::expansion), QtType.QStringList) - ) + val aliases = properties["Aliases"].into<QVariantMap>().orEmpty() - private fun initSetAliases(aliases: QVariantMap) { val names = aliases["names"].into<QStringList>().orEmpty() val expansions = aliases["expansions"].into<List<String>>().orEmpty() - require(names.size == expansions.size) { "Sizes do not match: names=${names.size}, expansions=${expansions.size}" } @@ -107,7 +104,7 @@ class AliasManager constructor( inline fun state() = flow().value @Suppress("NOTHING_TO_INLINE") - inline fun flow(): StateFlow<AliasManagerState> = state + inline fun flow() = state @PublishedApi internal val state = MutableStateFlow( diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BacklogManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BacklogManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BacklogManager.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferSyncer.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferSyncer.kt new file mode 100644 index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferSyncer.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfig.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfig.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewManager.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CertManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CertManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..f2d51725d618a804b628d6eebb445fc1056779c6 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CertManager.kt @@ -0,0 +1,126 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables + +import de.justjanne.libquassel.protocol.models.ids.IdentityId +import de.justjanne.libquassel.protocol.models.types.QtType +import de.justjanne.libquassel.protocol.serializers.qt.StringSerializerUtf8 +import de.justjanne.libquassel.protocol.syncables.state.CertManagerState +import de.justjanne.libquassel.protocol.syncables.stubs.CertManagerStub +import de.justjanne.libquassel.protocol.util.update +import de.justjanne.libquassel.protocol.variant.QVariantMap +import de.justjanne.libquassel.protocol.variant.into +import de.justjanne.libquassel.protocol.variant.qVariant +import kotlinx.coroutines.flow.MutableStateFlow +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.openssl.PEMKeyPair +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import java.nio.ByteBuffer +import java.security.PrivateKey +import java.security.cert.Certificate +import java.security.cert.CertificateFactory + +class CertManager( + identityId: IdentityId, + session: Session +) : SyncableObject(session, "CertManager"), CertManagerStub { + override fun fromVariantMap(properties: QVariantMap) { + val privateKeyPem = properties["sslKey"].into("") + val certPem = properties["sslCert"].into("") + + state.update { + copy( + privateKeyPem = privateKeyPem, + certificatePem = certPem, + privateKey = readPrivateKey(privateKeyPem), + certificate = readCertificate(certPem) + ) + } + } + + override fun toVariantMap() = mapOf( + "sslKey" to qVariant(StringSerializerUtf8.serializeRaw(state().certificatePem), QtType.QByteArray), + "sslCert" to qVariant(StringSerializerUtf8.serializeRaw(state().privateKeyPem), QtType.QByteArray) + ) + + private fun readPrivateKey(pem: String): PrivateKey? { + if (pem.isBlank()) { + return null + } + + try { + val keyPair = PEMParser(pem.reader()).readObject() as? PEMKeyPair + ?: return null + return JcaPEMKeyConverter().getPrivateKey(keyPair.privateKeyInfo) + } catch (t: Throwable) { + return null + } + } + + private fun readCertificate(pem: String): Certificate? { + if (pem.isBlank()) { + return null + } + + try { + val certificate = PEMParser(pem.reader()).readObject() as? X509CertificateHolder + ?: return null + return CertificateFactory.getInstance("X.509") + .generateCertificate(certificate.encoded.inputStream()) + } catch (t: Throwable) { + return null + } + } + + override fun setSslKey(encoded: ByteBuffer) { + val pem = StringSerializerUtf8.deserializeRaw(encoded) + + state.update { + copy( + privateKeyPem = pem, + privateKey = readPrivateKey(pem) + ) + } + super.setSslKey(encoded) + } + + override fun setSslCert(encoded: ByteBuffer) { + val pem = StringSerializerUtf8.deserializeRaw(encoded) + + state.update { + copy( + certificatePem = pem, + certificate = readCertificate(pem) + ) + } + super.setSslCert(encoded) + } + + fun privateKey() = state().privateKey + fun privateKeyPem() = state().privateKeyPem + + fun certificate() = state().certificate + fun certificatePem() = state().certificatePem + + @Suppress("NOTHING_TO_INLINE") + inline fun state() = flow().value + + @Suppress("NOTHING_TO_INLINE") + inline fun flow() = state + + @PublishedApi + internal val state = MutableStateFlow( + CertManagerState( + identityId = identityId + ) + ) +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CoreInfo.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CoreInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..95933ef3384e57b07a8958d014efa7475569de63 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CoreInfo.kt @@ -0,0 +1,82 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables + +import de.justjanne.libquassel.protocol.models.ConnectedClient +import de.justjanne.libquassel.protocol.models.types.QtType +import de.justjanne.libquassel.protocol.syncables.state.CoreInfoState +import de.justjanne.libquassel.protocol.syncables.stubs.CoreInfoStub +import de.justjanne.libquassel.protocol.util.update +import de.justjanne.libquassel.protocol.variant.QVariantList +import de.justjanne.libquassel.protocol.variant.QVariantMap +import de.justjanne.libquassel.protocol.variant.QVariant_ +import de.justjanne.libquassel.protocol.variant.into +import de.justjanne.libquassel.protocol.variant.qVariant +import kotlinx.coroutines.flow.MutableStateFlow +import org.threeten.bp.Instant + +class CoreInfo constructor( + session: Session +) : SyncableObject(session, "CoreInfo"), CoreInfoStub { + override fun fromVariantMap(properties: QVariantMap) { + val coreData = properties["coreData"].into<QVariantMap>().orEmpty() + + state.update { + copy( + version = coreData["quasselVersion"].into(version), + versionDate = coreData["quasselBuildDate"].into("") + .toLongOrNull() + ?.let(Instant::ofEpochSecond), + startTime = coreData["startTime"].into(startTime), + connectedClientCount = coreData["sessionConnectedClients"].into(connectedClientCount), + connectedClients = coreData["sessionConnectedClientData"].into<QVariantList>() + ?.mapNotNull<QVariant_, QVariantMap>(QVariant_::into) + ?.map(ConnectedClient.Companion::fromVariantMap) + .orEmpty() + ) + } + } + + override fun toVariantMap() = mapOf( + "quasselVersion" to qVariant(version(), QtType.QString), + "quasselBuildDate" to qVariant(versionDate()?.epochSecond?.toString(), QtType.QString), + "startTime" to qVariant(startTime(), QtType.QDateTime), + "sessionConnectedClients" to qVariant(connectedClientCount(), QtType.Int), + "sessionConnectedClientData" to qVariant( + connectedClients() + .map(ConnectedClient::toVariantMap) + .map { qVariant(it, QtType.QVariantMap) }, + QtType.QVariantList + ) + ) + + fun version() = state().version + fun versionDate() = state().versionDate + fun startTime() = state().startTime + fun connectedClientCount() = state().connectedClientCount + fun connectedClients() = state().connectedClients + + override fun setCoreData(data: QVariantMap) { + fromVariantMap(data) + super.setCoreData(data) + } + + @Suppress("NOTHING_TO_INLINE") + inline fun state() = flow().value + + @Suppress("NOTHING_TO_INLINE") + inline fun flow() = state + + @PublishedApi + internal val state = MutableStateFlow( + CoreInfoState() + ) +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/DccConfig.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/DccConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..991995bc6dad37bf036b3e47bc139e4982e22a28 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/DccConfig.kt @@ -0,0 +1,154 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables + +import de.justjanne.libquassel.protocol.models.DccIpDetectionMode +import de.justjanne.libquassel.protocol.models.DccPortSelectionMode +import de.justjanne.libquassel.protocol.models.types.QtType +import de.justjanne.libquassel.protocol.models.types.QuasselType +import de.justjanne.libquassel.protocol.syncables.state.DccConfigState +import de.justjanne.libquassel.protocol.syncables.stubs.DccConfigStub +import de.justjanne.libquassel.protocol.util.update +import de.justjanne.libquassel.protocol.variant.QVariantMap +import de.justjanne.libquassel.protocol.variant.into +import de.justjanne.libquassel.protocol.variant.qVariant +import kotlinx.coroutines.flow.MutableStateFlow +import java.net.InetAddress + +class DccConfig constructor( + session: Session +) : SyncableObject(session, "DccConfig"), DccConfigStub { + override fun init() { + renameObject("DccConfig") + } + + override fun fromVariantMap(properties: QVariantMap) { + state.update { + copy( + dccEnabled = properties["dccEnabled"].into(dccEnabled), + outgoingIp = properties["outgoingIp"].into(outgoingIp), + ipDetectionMode = properties["ipDetectionMode"].into(ipDetectionMode), + portSelectionMode = properties["portSelectionMode"].into(portSelectionMode), + minPort = properties["minPort"].into(minPort), + maxPort = properties["maxPort"].into(maxPort), + chunkSize = properties["chunkSize"].into(chunkSize), + sendTimeout = properties["sendTimeout"].into(sendTimeout), + usePassiveDcc = properties["usePassiveDcc"].into(usePassiveDcc), + useFastSend = properties["useFastSend"].into(useFastSend), + ) + } + } + + override fun toVariantMap() = mapOf( + "dccEnabled" to qVariant(state().dccEnabled, QtType.Bool), + "outgoingIp" to qVariant(state().outgoingIp, QuasselType.QHostAddress), + "ipDetectionMode" to qVariant(state().ipDetectionMode, QuasselType.DccConfigIpDetectionMode), + "portSelectionMode" to qVariant(state().portSelectionMode, QuasselType.DccConfigPortSelectionMode), + "minPort" to qVariant(state().minPort, QtType.UShort), + "maxPort" to qVariant(state().maxPort, QtType.UShort), + "chunkSize" to qVariant(state().chunkSize, QtType.Int), + "sendTimeout" to qVariant(state().sendTimeout, QtType.Int), + "usePassiveDcc" to qVariant(state().usePassiveDcc, QtType.Bool), + "useFastSend" to qVariant(state().useFastSend, QtType.Bool) + ) + + override fun setDccEnabled(enabled: Boolean) { + state.update { + copy(dccEnabled = enabled) + } + super.setDccEnabled(enabled) + } + + override fun setOutgoingIp(outgoingIp: InetAddress) { + state.update { + copy(outgoingIp = outgoingIp) + } + super.setOutgoingIp(outgoingIp) + } + + override fun setIpDetectionMode(ipDetectionMode: DccIpDetectionMode) { + state.update { + copy(ipDetectionMode = ipDetectionMode) + } + super.setIpDetectionMode(ipDetectionMode) + } + + override fun setPortSelectionMode(portSelectionMode: DccPortSelectionMode) { + state.update { + copy(portSelectionMode = portSelectionMode) + } + super.setPortSelectionMode(portSelectionMode) + } + + override fun setMinPort(port: UShort) { + state.update { + copy(minPort = port) + } + super.setMinPort(port) + } + + override fun setMaxPort(port: UShort) { + state.update { + copy(maxPort = port) + } + super.setMaxPort(port) + } + + override fun setChunkSize(chunkSize: Int) { + state.update { + copy(chunkSize = chunkSize) + } + super.setChunkSize(chunkSize) + } + + override fun setSendTimeout(timeout: Int) { + state.update { + copy(sendTimeout = timeout) + } + super.setSendTimeout(timeout) + } + + override fun setUsePassiveDcc(use: Boolean) { + state.update { + copy(usePassiveDcc = use) + } + super.setUsePassiveDcc(use) + } + + override fun setUseFastSend(use: Boolean) { + state.update { + copy(useFastSend = use) + } + super.setUseFastSend(use) + } + + fun isDccEnabled() = state().dccEnabled + fun outgoingIp() = state().outgoingIp + fun ipDetectionMode() = state().ipDetectionMode + fun portSelectionMode() = state().portSelectionMode + fun minPort() = state().minPort + fun maxPort() = state().maxPort + fun chunkSize() = state().chunkSize + fun sendTimeout() = state().sendTimeout + fun usePassiveDcc() = state().usePassiveDcc + fun useFastSend() = state().useFastSend + + @Suppress("NOTHING_TO_INLINE") + inline fun state() = flow().value + + @Suppress("NOTHING_TO_INLINE") + inline fun flow() = state + + @PublishedApi + internal val state = MutableStateFlow( + DccConfigState() + ) +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..20dd19ca5330faf6bbd25732dcf56d8f4a438e72 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt @@ -0,0 +1,214 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables + +import de.justjanne.libquassel.protocol.models.HighlightNickType +import de.justjanne.libquassel.protocol.models.HighlightRule +import de.justjanne.libquassel.protocol.models.QStringList +import de.justjanne.libquassel.protocol.models.types.QtType +import de.justjanne.libquassel.protocol.syncables.state.HighlightRuleManagerState +import de.justjanne.libquassel.protocol.syncables.stubs.HighlightRuleManagerStub +import de.justjanne.libquassel.protocol.util.update +import de.justjanne.libquassel.protocol.variant.QVariantList +import de.justjanne.libquassel.protocol.variant.QVariantMap +import de.justjanne.libquassel.protocol.variant.into +import de.justjanne.libquassel.protocol.variant.qVariant +import kotlinx.coroutines.flow.MutableStateFlow + +class HighlightRuleManager( + session: Session +) : SyncableObject(session, "HighlightRuleManager"), HighlightRuleManagerStub { + override fun fromVariantMap(properties: QVariantMap) { + val highlightRules = properties["HighlightRuleList"].into<QVariantMap>().orEmpty() + + val idList = highlightRules["id"].into<QVariantList>().orEmpty() + val nameList = highlightRules["name"].into<QStringList>().orEmpty() + val isRegExList = highlightRules["isRegEx"].into<QVariantList>().orEmpty() + val isCaseSensitiveList = highlightRules["isCaseSensitive"].into<QVariantList>().orEmpty() + val isEnabledList = highlightRules["isEnabled"].into<QVariantList>().orEmpty() + val isInverseList = highlightRules["isInverse"].into<QVariantList>().orEmpty() + val senderList = highlightRules["sender"].into<QStringList>().orEmpty() + val channelList = highlightRules["channel"].into<QStringList>().orEmpty() + + require(idList.size == nameList.size) { + "Sizes do not match: ids=${idList.size}, nameList=${nameList.size}" + } + require(idList.size == isRegExList.size) { + "Sizes do not match: ids=${idList.size}, isRegExList=${isRegExList.size}" + } + require(idList.size == isCaseSensitiveList.size) { + "Sizes do not match: ids=${idList.size}, isCaseSensitiveList=${isCaseSensitiveList.size}" + } + require(idList.size == isEnabledList.size) { + "Sizes do not match: ids=${idList.size}, isEnabledList=${isEnabledList.size}" + } + require(idList.size == isInverseList.size) { + "Sizes do not match: ids=${idList.size}, isInverseList=${isInverseList.size}" + } + require(idList.size == senderList.size) { + "Sizes do not match: ids=${idList.size}, senderList=${senderList.size}" + } + require(idList.size == channelList.size) { + "Sizes do not match: ids=${idList.size}, channelList=${channelList.size}" + } + require(idList.size == channelList.size) { + "Sizes do not match: ids=${idList.size}, channelList=${channelList.size}" + } + + state.update { + copy( + highlightNickType = properties["highlightNick"].into<Int>() + ?.let(HighlightNickType.Companion::of) + ?: highlightNickType, + highlightNickCaseSensitive = properties["nicksCaseSensitive"].into(highlightNickCaseSensitive), + rules = List(idList.size) { + HighlightRule( + idList[it].into(0), + nameList[it] ?: "", + isRegEx = isRegExList[it].into(false), + isCaseSensitive = isCaseSensitiveList[it].into(false), + isEnabled = isEnabledList[it].into(false), + isInverse = isInverseList[it].into(false), + sender = senderList[it] ?: "", + channel = channelList[it] ?: "" + ) + } + ) + } + } + + override fun toVariantMap() = mapOf( + "HighlightRuleList" to qVariant( + mapOf( + "id" to qVariant( + state().rules.map { qVariant(it.id, QtType.Int) }, + QtType.QVariantList + ), + "name" to qVariant( + state().rules.map(HighlightRule::contents), + QtType.QStringList + ), + "isRegEx" to qVariant( + state().rules.map { qVariant(it.isRegEx, QtType.Bool) }, + QtType.QVariantList + ), + "isCaseSensitive" to qVariant( + state().rules.map { qVariant(it.isCaseSensitive, QtType.Bool) }, + QtType.QVariantList + ), + "isEnabled" to qVariant( + state().rules.map { qVariant(it.isEnabled, QtType.Bool) }, + QtType.QVariantList + ), + "isInverse" to qVariant( + state().rules.map { qVariant(it.isInverse, QtType.Bool) }, + QtType.QVariantList + ), + "sender" to qVariant( + state().rules.map(HighlightRule::sender), + QtType.QStringList + ), + "channel" to qVariant( + state().rules.map(HighlightRule::channel), + QtType.QStringList + ), + ), + QtType.QVariantMap + ), + "highlightNick" to qVariant(state().highlightNickType.value, QtType.Int), + "nicksCaseSensitive" to qVariant(state().highlightNickCaseSensitive, QtType.Bool) + ) + + fun indexOf(id: Int): Int = state().indexOf(id) + fun contains(id: Int) = state().contains(id) + + fun isEmpty() = state().isEmpty() + fun count() = state().count() + fun removeAt(index: Int) { + state.update { + copy(rules = rules.drop(index)) + } + } + + override fun removeHighlightRule(highlightRule: Int) { + removeAt(indexOf(highlightRule)) + super.removeHighlightRule(highlightRule) + } + + override fun toggleHighlightRule(highlightRule: Int) { + state.update { + copy( + rules = rules.map { + if (it.id != highlightRule) it + else it.copy(isEnabled = !it.isEnabled) + } + ) + } + super.toggleHighlightRule(highlightRule) + } + + override fun addHighlightRule( + id: Int, + name: String?, + isRegEx: Boolean, + isCaseSensitive: Boolean, + isEnabled: Boolean, + isInverse: Boolean, + sender: String?, + chanName: String? + ) { + if (contains(id)) { + return + } + + state.update { + copy( + rules = rules + HighlightRule( + id, + name ?: "", + isRegEx, + isCaseSensitive, + isEnabled, + isInverse, + sender ?: "", + chanName ?: "" + ) + ) + } + + super.addHighlightRule(id, name, isRegEx, isCaseSensitive, isEnabled, isInverse, sender, chanName) + } + + override fun setHighlightNick(highlightNick: Int) { + state.update { + copy(highlightNickType = HighlightNickType.of(highlightNick) ?: highlightNickType) + } + super.setHighlightNick(highlightNick) + } + + override fun setNicksCaseSensitive(nicksCaseSensitive: Boolean) { + state.update { + copy(highlightNickCaseSensitive = nicksCaseSensitive) + } + super.setNicksCaseSensitive(nicksCaseSensitive) + } + + @Suppress("NOTHING_TO_INLINE") + inline fun state() = flow().value + + @Suppress("NOTHING_TO_INLINE") + inline fun flow() = state + + @PublishedApi + internal val state = MutableStateFlow( + HighlightRuleManagerState() + ) +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Identity.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Identity.kt new file mode 100644 index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Identity.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt index fbbfd8ad83e4e00ad4957749d149b94c5a8156bb..885b4f01a8490bb7bc4988d79d6177830317358c 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt @@ -23,7 +23,6 @@ import de.justjanne.libquassel.protocol.variant.indexed import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.qVariant import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class IrcChannel( name: String, @@ -269,7 +268,7 @@ class IrcChannel( inline fun state() = flow().value @Suppress("NOTHING_TO_INLINE") - inline fun flow(): StateFlow<IrcChannelState> = state + inline fun flow() = state @PublishedApi internal val state = MutableStateFlow( diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcListHelper.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcListHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcListHelper.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt index c492dc53be450ca32d2290d6b6c3ca7320d5e570..115f120ae5d5ad0a11d9d62d0627f6ca35234ae1 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt @@ -21,7 +21,6 @@ import de.justjanne.libquassel.protocol.variant.indexed import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.qVariant import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import org.threeten.bp.Instant import org.threeten.bp.temporal.Temporal @@ -307,7 +306,7 @@ class IrcUser( inline fun state() = flow().value @Suppress("NOTHING_TO_INLINE") - inline fun flow(): StateFlow<IrcUserState> = state + inline fun flow() = state @PublishedApi internal val state = MutableStateFlow( diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt index 9fdd6fe259aa78efc09c273682f81e1ed279ecbc..80bbef5781fd70c4586be3a4b69df7bdd4e2e846 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt @@ -32,7 +32,6 @@ import de.justjanne.libquassel.protocol.variant.QVariantMap import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.qVariant import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import java.nio.ByteBuffer class Network constructor( @@ -727,7 +726,7 @@ class Network constructor( inline fun state() = flow().value @Suppress("NOTHING_TO_INLINE") - inline fun flow(): StateFlow<NetworkState> = state + inline fun flow() = state @PublishedApi internal val state = MutableStateFlow( diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/NetworkConfig.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/NetworkConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/NetworkConfig.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferSyncerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferSyncerState.kt new file mode 100644 index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferSyncerState.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewConfigState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewConfigState.kt new file mode 100644 index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewConfigState.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewManagerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewManagerState.kt new file mode 100644 index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewManagerState.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CertManagerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CertManagerState.kt new file mode 100644 index 0000000000000000000000000000000000000000..c129d46671bf0261c42719309a463cf2bc60ab15 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CertManagerState.kt @@ -0,0 +1,23 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state + +import de.justjanne.libquassel.protocol.models.ids.IdentityId +import java.security.PrivateKey +import java.security.cert.Certificate + +data class CertManagerState( + val identityId: IdentityId, + val privateKeyPem: String = "", + val certificatePem: String = "", + val privateKey: PrivateKey? = null, + val certificate: Certificate? = null +) diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CoreInfoState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CoreInfoState.kt new file mode 100644 index 0000000000000000000000000000000000000000..f09dbd464b16701a76c7cec3fee700cf3a54e71e --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CoreInfoState.kt @@ -0,0 +1,22 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state + +import de.justjanne.libquassel.protocol.models.ConnectedClient +import org.threeten.bp.Instant + +data class CoreInfoState( + val version: String = "", + val versionDate: Instant? = null, + val startTime: Instant = Instant.EPOCH, + val connectedClientCount: Int = 0, + val connectedClients: List<ConnectedClient> = emptyList() +) diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/DccConfigState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/DccConfigState.kt new file mode 100644 index 0000000000000000000000000000000000000000..09f14fdf64c28a27b69ce2bc518b14952f1fb7cf --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/DccConfigState.kt @@ -0,0 +1,28 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state + +import de.justjanne.libquassel.protocol.models.DccIpDetectionMode +import de.justjanne.libquassel.protocol.models.DccPortSelectionMode +import java.net.InetAddress + +data class DccConfigState( + val dccEnabled: Boolean = false, + val outgoingIp: InetAddress = InetAddress.getLocalHost(), + val ipDetectionMode: DccIpDetectionMode = DccIpDetectionMode.Automatic, + val portSelectionMode: DccPortSelectionMode = DccPortSelectionMode.Automatic, + val minPort: UShort = 1024u, + val maxPort: UShort = 32767u, + val chunkSize: Int = 16, + val sendTimeout: Int = 180, + val usePassiveDcc: Boolean = false, + val useFastSend: Boolean = false +) diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/HighlightRuleManagerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/HighlightRuleManagerState.kt new file mode 100644 index 0000000000000000000000000000000000000000..6401a56f3b4616d5c72bb631dbd8ce312ffc9a9a --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/HighlightRuleManagerState.kt @@ -0,0 +1,87 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state + +import de.justjanne.libquassel.protocol.models.HighlightNickType +import de.justjanne.libquassel.protocol.models.HighlightRule +import de.justjanne.libquassel.protocol.models.flags.MessageFlag +import de.justjanne.libquassel.protocol.models.flags.MessageFlags +import de.justjanne.libquassel.protocol.models.flags.MessageType +import de.justjanne.libquassel.protocol.models.flags.MessageTypes +import de.justjanne.libquassel.protocol.util.expression.ExpressionMatch +import de.justjanne.libquassel.protocol.util.irc.IrcFormatDeserializer + +data class HighlightRuleManagerState( + val rules: List<HighlightRule> = emptyList(), + val highlightNickType: HighlightNickType = HighlightNickType.CurrentNick, + val highlightNickCaseSensitive: Boolean = false +) { + fun match( + message: String, + sender: String, + type: MessageTypes, + flags: MessageFlags, + bufferName: String, + currentNick: String, + identityNicks: List<String> + ): Boolean { + val messageContent = IrcFormatDeserializer.stripColors(message) + + if (!type.contains(MessageType.Action) && + !type.contains(MessageType.Notice) && + !type.contains(MessageType.Plain) + ) { + return false + } + + if (flags.contains(MessageFlag.Self)) { + return false + } + + val matchingRules = rules.asSequence() + .filter { it.isEnabled } + .filter { it.channelMatch.match(bufferName, true) } + .filter { it.senderMatch.match(sender, true) } + .filter { it.contentMatch.match(messageContent, true) } + .toList() + + if (matchingRules.any(HighlightRule::isInverse)) { + return false + } + + if (matchingRules.isNotEmpty()) { + return true + } + + val nicks = when (highlightNickType) { + HighlightNickType.NoNick -> return false + HighlightNickType.CurrentNick -> listOf(currentNick) + HighlightNickType.AllNicks -> identityNicks + currentNick + }.filter(String::isNotBlank) + + if (nicks.isNotEmpty() && ExpressionMatch( + nicks.joinToString("\n"), + ExpressionMatch.MatchMode.MatchMultiPhrase, + highlightNickCaseSensitive + ).match(messageContent) + ) { + return true + } + + return false + } + + fun indexOf(id: Int): Int = rules.indexOfFirst { it.id == id } + fun contains(id: Int) = rules.any { it.id == id } + + fun isEmpty() = rules.isEmpty() + fun count() = rules.size +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IdentityState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IdentityState.kt new file mode 100644 index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IdentityState.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcListHelperState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcListHelperState.kt new file mode 100644 index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcListHelperState.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkConfigState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkConfigState.kt new file mode 100644 index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkConfigState.kt @@ -0,0 +1,11 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.syncables.state diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatch.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatch.kt new file mode 100644 index 0000000000000000000000000000000000000000..b13da211c7d48f8893aace02cb458f3816d57e3c --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatch.kt @@ -0,0 +1,259 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.util.expression + +/** + * Construct an Expression match with the given parameters + * + * @param expression A phrase, wildcard expression, or regular expression + * @param mode Expression matching mode @see ExpressionMatch.MatchMode + * @param caseSensitive If true, match case-sensitively, otherwise ignore case when matching + */ +data class ExpressionMatch( + val expression: String, + val mode: MatchMode, + val caseSensitive: Boolean +) { + enum class MatchMode { + MatchPhrase, + MatchMultiPhrase, + MatchWildcard, + MatchMultiWildcard, + MatchRegEx + } + + internal val positiveRegex: Regex? + internal val negativeRegex: Regex? + + init { + val (positive, negative) = when (mode) { + MatchMode.MatchPhrase -> parsePhrase(expression) + MatchMode.MatchMultiPhrase -> parseMultiPhrase(expression) + MatchMode.MatchWildcard -> parseWildcard(expression) + MatchMode.MatchMultiWildcard -> parseMultiWildcard(expression) + MatchMode.MatchRegEx -> parseRegEx(expression) + } + + positiveRegex = regex(positive, caseSensitive = caseSensitive) + negativeRegex = regex(negative, caseSensitive = caseSensitive) + } + + fun isEmpty() = positiveRegex == null && negativeRegex == null + + fun match(content: String, matchEmpty: Boolean = false): Boolean { + if (isEmpty()) { + return matchEmpty + } + + if (negativeRegex != null && negativeRegex.containsMatchIn(content)) { + return false + } + + if (positiveRegex != null) { + return positiveRegex.containsMatchIn(content) + } + + return true + } + + companion object { + private fun regex(expression: String, caseSensitive: Boolean): Regex? = try { + when { + expression.isBlank() -> null + caseSensitive -> expression.toRegex() + else -> expression.toRegex(RegexOption.IGNORE_CASE) + } + } catch (t: Throwable) { + null + } + + internal fun splitWithEscaping(expression: String): List<String> { + val result = mutableListOf<String>() + val current = StringBuilder() + var escaped = false + for (char in expression) { + if (escaped) { + when (char) { + '\\' -> { + current.append(char) + current.append(char) + escaped = false + } + '\n' -> { + current.append("\\") + result.add(current.toString()) + current.clear() + escaped = false + } + else -> { + current.append("\\") + current.append(char) + escaped = false + } + } + } else { + when (char) { + ';', '\n' -> { + result.add(current.toString()) + current.clear() + } + '\\' -> { + escaped = true + } + else -> { + current.append(char) + } + } + } + } + result.add(current.toString()) + return result + } + + private fun parseInverted(expression: String): Pair<Boolean, String> { + if (expression.startsWith('!')) { + return Pair(true, expression.substring(1)) + } + + if (expression.startsWith("\\!")) { + return Pair(false, expression.substring(1)) + } + + return Pair(false, expression) + } + + private fun escape(expression: String): String { + val result = StringBuilder(expression.length) + for (char in expression) { + when (char) { + '\\', '.', '[', ']', '{', '}', '(', ')', '<', '>', '*', '+', '-', '=', '?', '^', '$', '|' -> { + result.append('\\') + result.append(char) + } + else -> { + result.append(char) + } + } + } + return result.toString() + } + + private fun parsePhrase(expression: String): Pair<String, String> { + if (expression.isBlank()) { + return Pair("", "") + } + + return Pair("(?:^|\\W)${escape(expression)}(?:\\W|$)", "") + } + + private fun parseMultiPhrase(expression: String): Pair<String, String> { + val components = expression.split('\n') + .filter(String::isNotEmpty) + .map(::escape) + + if (components.isEmpty()) { + return Pair("", "") + } + + return Pair(components.joinToString("|", prefix = "(?:^|\\W)(?:", postfix = ")(?:\\W|$)"), "") + } + + private fun parseWildcardInternal(expression: String): String { + val result = StringBuilder() + var escaped = false + for (char in expression) { + when (char) { + '\\' -> if (escaped) { + result.append('\\') + result.append(char) + escaped = false + } else { + escaped = true + } + '?' -> if (escaped) { + result.append('\\') + result.append(char) + escaped = false + } else { + result.append('.') + escaped = false + } + '*' -> if (escaped) { + result.append('\\') + result.append(char) + escaped = false + } else { + result.append(".*") + escaped = false + } + '.', '[', ']', '{', '}', '(', ')', '<', '>', '+', '-', '=', '^', '$', '|' -> { + result.append('\\') + result.append(char) + escaped = false + } + else -> { + result.append(char) + escaped = false + } + } + } + return result.toString() + } + + private fun parseWildcard(expression: String): Pair<String, String> { + val (inverted, phrase) = parseInverted(expression) + val result = "^${parseWildcardInternal(phrase)}$" + + return if (inverted) Pair("", result) + else Pair(result, "") + } + + private fun parseMultiWildcard(expression: String): Pair<String, String> { + val components = splitWithEscaping(expression) + + val positive = components + .asSequence() + .map(::parseInverted) + .filterNot(Pair<Boolean, String>::first) + .map(Pair<Boolean, String>::second) + .map(String::trim) + .map(::parseWildcardInternal) + .filter(String::isNotEmpty) + .toList() + + val negative = components + .asSequence() + .map(::parseInverted) + .filter(Pair<Boolean, String>::first) + .map(Pair<Boolean, String>::second) + .map(String::trim) + .map(::parseWildcardInternal) + .filter(String::isNotEmpty) + .toList() + + return Pair( + if (positive.isEmpty()) "" + else positive.joinToString("|", prefix = "^(?:", postfix = ")$"), + if (negative.isEmpty()) "" + else negative.joinToString("|", prefix = "^(?:", postfix = ")$") + ) + } + + private fun parseRegEx(expression: String): Pair<String, String> { + val (inverted, phrase) = parseInverted(expression) + return when { + phrase.isBlank() -> return Pair("", "") + inverted -> Pair("", phrase) + else -> Pair(phrase, "") + } + } + } +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfo.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..6b2ab49bf9d0b9311dc8ae2f698c38b3fe1d2c14 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfo.kt @@ -0,0 +1,16 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.util.irc + +data class FormatInfo<T : IrcFormat>( + val range: IntRange, + val format: T +) diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfoBuilder.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfoBuilder.kt new file mode 100644 index 0000000000000000000000000000000000000000..be2a6246178fcc913d528a66a7324422cd03d461 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfoBuilder.kt @@ -0,0 +1,16 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.util.irc + +data class FormatInfoBuilder<T : IrcFormat>( + val start: Int, + val format: T +) diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormat.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormat.kt new file mode 100644 index 0000000000000000000000000000000000000000..db64adde33b3f867d7defd12fcd5d008abb54c80 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormat.kt @@ -0,0 +1,35 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.util.irc + +sealed class IrcFormat { + object Italic : IrcFormat() + + object Underline : IrcFormat() + + object Strikethrough : IrcFormat() + + object Monospace : IrcFormat() + + object Bold : IrcFormat() + + data class HexColor( + val foreground: UInt, + val background: UInt + ) : IrcFormat() + + data class IrcColor( + val foreground: UByte, + val background: UByte + ) : IrcFormat() { + fun copySwapped() = copy(foreground = background, background = foreground) + } +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormatDeserializer.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormatDeserializer.kt new file mode 100644 index 0000000000000000000000000000000000000000..69ea0034c990738edd3fc4b9d83f239b5fd8230f --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormatDeserializer.kt @@ -0,0 +1,379 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2020 Janne Mareike Koschinski + * Copyright (c) 2020 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.justjanne.libquassel.protocol.util.irc + +object IrcFormatDeserializer { + fun stripColors( + str: String + ) = formatString(str, false, mutableListOf()) + + fun formatString( + str: String, + colorize: Boolean + ): Pair<String, List<FormatInfo<IrcFormat>>> { + val list = mutableListOf<FormatInfo<IrcFormat>>() + val content = formatString(str, colorize, list) + return Pair(content, list) + } + + fun formatString( + str: String?, + colorize: Boolean, + output: MutableList<FormatInfo<IrcFormat>> + ): String { + if (str == null) return "" + + val plainText = StringBuilder() + var bold: FormatInfoBuilder<IrcFormat.Bold>? = null + var italic: FormatInfoBuilder<IrcFormat.Italic>? = null + var underline: FormatInfoBuilder<IrcFormat.Underline>? = null + var strikethrough: FormatInfoBuilder<IrcFormat.Strikethrough>? = null + var monospace: FormatInfoBuilder<IrcFormat.Monospace>? = null + var color: FormatInfoBuilder<IrcFormat.IrcColor>? = null + var hexColor: FormatInfoBuilder<IrcFormat.HexColor>? = null + + fun applyFormat(desc: FormatInfoBuilder<*>) { + output.add(FormatInfo(desc.start..plainText.length, desc.format)) + } + + // Iterating over every character + var normalCount = 0 + var i = 0 + while (i < str.length) { + val character = str[i] + when (character) { + CODE_BOLD -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If there is an element on stack with the same code, close it + bold = if (bold != null) { + if (colorize) applyFormat(bold) + null + // Otherwise create a new one + } else { + FormatInfoBuilder( + plainText.length, + IrcFormat.Bold + ) + } + } + CODE_ITALIC -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If there is an element on stack with the same code, close it + italic = if (italic != null) { + if (colorize) applyFormat(italic) + null + // Otherwise create a new one + } else { + FormatInfoBuilder( + plainText.length, + IrcFormat.Italic + ) + } + } + CODE_UNDERLINE -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If there is an element on stack with the same code, close it + underline = if (underline != null) { + if (colorize) applyFormat(underline) + null + // Otherwise create a new one + } else { + FormatInfoBuilder( + plainText.length, + IrcFormat.Underline + ) + } + } + CODE_STRIKETHROUGH -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If there is an element on stack with the same code, close it + strikethrough = if (strikethrough != null) { + if (colorize) applyFormat(strikethrough) + null + // Otherwise create a new one + } else { + FormatInfoBuilder( + plainText.length, + IrcFormat.Strikethrough + ) + } + } + CODE_MONOSPACE -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If there is an element on stack with the same code, close it + monospace = if (monospace != null) { + if (colorize) applyFormat(monospace) + null + // Otherwise create a new one + } else { + FormatInfoBuilder( + plainText.length, + IrcFormat.Monospace + ) + } + } + CODE_COLOR -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + val foregroundStart = i + 1 + val foregroundEnd = findEndOfNumber(str, foregroundStart) + // If we have a foreground element + if (foregroundEnd > foregroundStart) { + val foreground = readNumber(str, foregroundStart, foregroundEnd) + + var background: UByte? = null + var backgroundEnd = -1 + // If we have a background code, read it + if (str.length > foregroundEnd && str[foregroundEnd] == ',') { + backgroundEnd = findEndOfNumber(str, foregroundEnd + 1) + background = readNumber(str, foregroundEnd + 1, backgroundEnd) + } + // If previous element was also a color element, try to reuse background + if (color != null) { + // Apply old format + if (colorize) applyFormat(color) + // Reuse old background, if possible + if (background == null) + background = color.format.background + } + // Add new format + color = FormatInfoBuilder( + plainText.length, + IrcFormat.IrcColor( + foreground ?: 0xFFu, + background ?: 0xFFu + ) + ) + + // i points in front of the next character + i = (if (backgroundEnd == -1) foregroundEnd else backgroundEnd) - 1 + + // Otherwise assume this is a closing tag + } else if (color != null) { + if (colorize) applyFormat(color) + color = null + } + } + CODE_HEXCOLOR -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + val foregroundStart = i + 1 + val foregroundEnd = findEndOfHexNumber(str, foregroundStart) + // If we have a foreground element + if (foregroundEnd > foregroundStart) { + val foreground = readHexNumber(str, foregroundStart, foregroundEnd) + + var background: UInt? = null + var backgroundEnd = -1 + // If we have a background code, read it + if (str.length > foregroundEnd && str[foregroundEnd] == ',') { + backgroundEnd = findEndOfHexNumber(str, foregroundEnd + 1) + background = readHexNumber(str, foregroundEnd + 1, backgroundEnd) + } + // If previous element was also a color element, try to reuse background + if (hexColor != null) { + // Apply old format + if (colorize) applyFormat(hexColor) + // Reuse old background, if possible + if (background == null) + background = hexColor.format.background + } + // Add new format + hexColor = FormatInfoBuilder( + plainText.length, + IrcFormat.HexColor( + foreground ?: 0xFFFFFFFFu, + background ?: 0xFFFFFFFFu + ) + ) + + // i points in front of the next character + i = (if (backgroundEnd == -1) foregroundEnd else backgroundEnd) - 1 + + // Otherwise assume this is a closing tag + } else if (hexColor != null) { + if (colorize) applyFormat(hexColor) + hexColor = null + } + } + CODE_SWAP -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If we have a color tag before, apply it, and create a new one with swapped colors + if (color != null) { + if (colorize) applyFormat(color) + color = FormatInfoBuilder( + plainText.length, color.format.copySwapped() + ) + } + } + CODE_RESET -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // End all formatting tags + if (bold != null) { + if (colorize) applyFormat(bold) + bold = null + } + if (italic != null) { + if (colorize) applyFormat(italic) + italic = null + } + if (underline != null) { + if (colorize) applyFormat(underline) + underline = null + } + if (color != null) { + if (colorize) applyFormat(color) + color = null + } + if (hexColor != null) { + if (colorize) applyFormat(hexColor) + hexColor = null + } + } + else -> { + // Just append it, if it’s not special + normalCount++ + } + } + i++ + } + + plainText.append(str.substring(str.length - normalCount, str.length)) + + // End all formatting tags + if (bold != null) { + if (colorize) applyFormat(bold) + } + if (italic != null) { + if (colorize) applyFormat(italic) + } + if (underline != null) { + if (colorize) applyFormat(underline) + } + if (strikethrough != null) { + if (colorize) applyFormat(strikethrough) + } + if (monospace != null) { + if (colorize) applyFormat(monospace) + } + if (color != null) { + if (colorize) applyFormat(color) + } + if (hexColor != null) { + if (colorize) applyFormat(hexColor) + } + return plainText.toString() + } + + private const val CODE_BOLD = 0x02.toChar() + private const val CODE_COLOR = 0x03.toChar() + private const val CODE_HEXCOLOR = 0x04.toChar() + private const val CODE_ITALIC = 0x1D.toChar() + private const val CODE_UNDERLINE = 0x1F.toChar() + private const val CODE_STRIKETHROUGH = 0x1E.toChar() + private const val CODE_MONOSPACE = 0x11.toChar() + private const val CODE_SWAP = 0x16.toChar() + private const val CODE_RESET = 0x0F.toChar() + + /** + * Try to read a number from a String in specified bounds + * + * @param str String to be read from + * @param start Start index (inclusive) + * @param end End index (exclusive) + * @return The byte represented by the digits read from the string + */ + fun readNumber(str: String, start: Int, end: Int): UByte? { + val result = str.substring(start, end) + return if (result.isEmpty()) null + else result.toUByteOrNull(10) + } + + /** + * Try to read a number from a String in specified bounds + * + * @param str String to be read from + * @param start Start index (inclusive) + * @param end End index (exclusive) + * @return The byte represented by the digits read from the string + */ + fun readHexNumber(str: String, start: Int, end: Int): UInt? { + val result = str.substring(start, end) + return if (result.isEmpty()) null + else result.toUIntOrNull(16) + } + + /** + * @param str String to be searched in + * @param start Start position (inclusive) + * @return Index of first character that is not a digit + */ + private fun findEndOfNumber(str: String, start: Int): Int { + val searchFrame = str.substring(start) + var i = 0 + loop@ while (i < 2 && i < searchFrame.length) { + when (searchFrame[i]) { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> { + // Do nothing + } + else -> break@loop + } + i++ + } + return start + i + } + + /** + * @param str String to be searched in + * @param start Start position (inclusive) + * @return Index of first character that is not a digit + */ + private fun findEndOfHexNumber(str: String, start: Int): Int { + val searchFrame = str.substring(start) + var i = 0 + loop@ while (i < 6 && i < searchFrame.length) { + when (searchFrame[i]) { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', + 'c', 'd', 'e', 'f' -> { + // Do nothing + } + else -> break@loop + } + i++ + } + return start + i + } +} diff --git a/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatchTest.kt b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatchTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..a485e5a33a74747638830c145a23b3aeaef42037 --- /dev/null +++ b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatchTest.kt @@ -0,0 +1,477 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.util.expression + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class ExpressionMatchTest { + @Test + fun testEmptyPattern() { + // Empty pattern + val emptyMatch = ExpressionMatch("", ExpressionMatch.MatchMode.MatchPhrase, false) + assertNull(emptyMatch.positiveRegex) + assertNull(emptyMatch.negativeRegex) + // Assert empty is valid + // Assert empty + assertTrue(emptyMatch.isEmpty()) + // Assert default match fails (same as setting match empty to false) + assertFalse(emptyMatch.match("something")) + // Assert match empty succeeds + assertTrue(emptyMatch.match("something", true)) + } + + @Test + fun testMatchPhrase() { + // Simple phrase, case-insensitive + val simpleMatch = ExpressionMatch("test", ExpressionMatch.MatchMode.MatchPhrase, false) + // Simple phrase, case-sensitive + val simpleMatchCS = ExpressionMatch("test", ExpressionMatch.MatchMode.MatchPhrase, true) + // Phrase with space, case-insensitive + val simpleMatchSpace = ExpressionMatch(" space ", ExpressionMatch.MatchMode.MatchPhrase, true) + // Complex phrase + val complexMatchFull = """^(?:norm|norm\-space|\!norm\-escaped|\\\!slash\-invert|\\\\double|escape\;""" + + """sep|slash\-end\-split\\|quad\\\\\!noninvert|newline\-split|newline\-split\-slash\\|slash\-at\-end\\)$""" + val complexMatch = ExpressionMatch( + complexMatchFull, + ExpressionMatch.MatchMode.MatchPhrase, + false + ) + + assertEquals("(?:^|\\W)test(?:\\W|$)", simpleMatch.positiveRegex?.pattern) + assertEquals(setOf(RegexOption.IGNORE_CASE), simpleMatch.positiveRegex?.options) + assertNull(simpleMatch.negativeRegex) + + assertEquals("(?:^|\\W)test(?:\\W|$)", simpleMatchCS.positiveRegex?.pattern) + assertEquals(emptySet<RegexOption>(), simpleMatchCS.positiveRegex?.options) + assertNull(simpleMatchCS.negativeRegex) + + assertEquals("(?:^|\\W) space (?:\\W|$)", simpleMatchSpace.positiveRegex?.pattern) + assertEquals(emptySet<RegexOption>(), simpleMatchSpace.positiveRegex?.options) + assertNull(simpleMatchSpace.negativeRegex) + + // Assert valid and not empty + assertFalse(simpleMatch.isEmpty()) + assertFalse(simpleMatchCS.isEmpty()) + assertFalse(simpleMatchSpace.isEmpty()) + assertFalse(complexMatch.isEmpty()) + + // Assert match succeeds + assertTrue(simpleMatch.match("test")) + assertTrue(simpleMatch.match("other test;")) + assertTrue(simpleMatchSpace.match(" space ")) + // Assert partial match fails + assertFalse(simpleMatch.match("testing")) + assertFalse(simpleMatchSpace.match("space")) + // Assert unrelated fails + assertFalse(simpleMatch.match("not above")) + + // Assert case sensitivity followed + assertFalse(simpleMatch.caseSensitive) + assertTrue(simpleMatch.match("TeSt")) + assertTrue(simpleMatchCS.caseSensitive) + assertFalse(simpleMatchCS.match("TeSt")) + + // Assert complex phrases are escaped properly + assertTrue(complexMatch.match(complexMatchFull)) + assertFalse(complexMatch.match("norm")) + } + + @Test + fun matchMultiPhrase() { + val emptyMatch = + ExpressionMatch( + "\n\n", + ExpressionMatch.MatchMode.MatchMultiPhrase, false + ) + // Simple phrases, case-insensitive + val simpleMatch = ExpressionMatch( + "test\nOther ", + ExpressionMatch.MatchMode.MatchMultiPhrase, + false + ) + // Simple phrases, case-sensitive + val simpleMatchCS = ExpressionMatch( + "test\nOther ", + ExpressionMatch.MatchMode.MatchMultiPhrase, + true + ) + // Complex phrases + val complexMatchFullA = + """^(?:norm|norm\-space|\!norm\-escaped|\\\!slash\-invert|\\\\double)|escape\;""" + + """sep|slash\-end\-split\\|quad\\\\\!noninvert)|newline\-split|newline\-split\-slash\\|slash\-at\-end\\)$""" + val complexMatchFullB = """^(?:invert|invert\-space)$)$""" + val complexMatch = ExpressionMatch( + complexMatchFullA + "\n" + complexMatchFullB, + ExpressionMatch.MatchMode.MatchMultiPhrase, + false + ) + + println(simpleMatch.positiveRegex) + println(simpleMatch.negativeRegex) + + // Assert valid and not empty + assertFalse(simpleMatch.isEmpty()) + assertFalse(simpleMatchCS.isEmpty()) + assertFalse(complexMatch.isEmpty()) + + // Assert match succeeds + assertTrue(simpleMatch.match("test")) + assertTrue(simpleMatch.match("test[suffix]")) + assertTrue(simpleMatch.match("other test;")) + assertTrue(simpleMatch.match("Other ")) + assertTrue(simpleMatch.match(".Other !")) + // Assert partial match fails + assertFalse(simpleMatch.match("testing")) + assertFalse(simpleMatch.match("Other!")) + // Assert unrelated fails + assertFalse(simpleMatch.match("not above")) + + // Assert case sensitivity followed + assertFalse(simpleMatch.caseSensitive) + assertTrue(simpleMatch.match("TeSt")) + assertTrue(simpleMatchCS.caseSensitive) + assertFalse(simpleMatchCS.match("TeSt")) + + // Assert complex phrases are escaped properly + assertTrue(complexMatch.match(complexMatchFullA)) + assertTrue(complexMatch.match(complexMatchFullB)) + assertFalse(complexMatch.match("norm")) + assertFalse(complexMatch.match("invert")) + } + + @Test + fun matchWildcard() { + // Simple wildcard, case-insensitive + val simpleMatch = + ExpressionMatch("?test*", ExpressionMatch.MatchMode.MatchWildcard, false) + // Simple wildcard, case-sensitive + val simpleMatchCS = + ExpressionMatch("?test*", ExpressionMatch.MatchMode.MatchWildcard, true) + // Escaped wildcard, case-insensitive + val simpleMatchEscape = + ExpressionMatch("""\?test\*""", ExpressionMatch.MatchMode.MatchWildcard, false) + // Inverted wildcard, case-insensitive + val simpleMatchInvert = + ExpressionMatch("!test*", ExpressionMatch.MatchMode.MatchWildcard, false) + // Not inverted wildcard, case-insensitive + val simpleMatchNoInvert = + ExpressionMatch("""\!test*""", ExpressionMatch.MatchMode.MatchWildcard, false) + // Not inverted wildcard literal slash, case-insensitive + val simpleMatchNoInvertSlash = + ExpressionMatch("""\\!test*""", ExpressionMatch.MatchMode.MatchWildcard, false) + // Complex wildcard + val complexMatch = + ExpressionMatch( + """never?gonna*give\*you\?up\\test|y\yeah\\1\\\\2\\\1inval""", + ExpressionMatch.MatchMode.MatchWildcard, false + ) + + // Assert valid and not empty + assertFalse(simpleMatch.isEmpty()) + assertFalse(simpleMatchCS.isEmpty()) + assertFalse(simpleMatchEscape.isEmpty()) + assertFalse(simpleMatchInvert.isEmpty()) + assertFalse(simpleMatchNoInvert.isEmpty()) + assertFalse(simpleMatchNoInvertSlash.isEmpty()) + assertFalse(complexMatch.isEmpty()) + + // Assert match succeeds + assertTrue(simpleMatch.match("@test")) + assertTrue(simpleMatch.match("@testing")) + assertTrue(simpleMatch.match("!test")) + assertTrue(simpleMatchEscape.match("?test*")) + assertTrue(simpleMatchInvert.match("atest")) + assertTrue(simpleMatchNoInvert.match("!test")) + assertTrue(simpleMatchNoInvertSlash.match("""\!test)""")) + // Assert partial match fails + assertFalse(simpleMatch.match("test")) + // Assert unrelated fails + assertFalse(simpleMatch.match("not above")) + // Assert escaped wildcard fails + assertFalse(simpleMatchEscape.match("@testing")) + assertFalse(simpleMatchNoInvert.match("test")) + assertFalse(simpleMatchNoInvert.match("anything")) + assertFalse(simpleMatchNoInvertSlash.match("!test")) + assertFalse(simpleMatchNoInvertSlash.match("test")) + assertFalse(simpleMatchNoInvertSlash.match("anything")) + // Assert non-inverted fails + assertFalse(simpleMatchInvert.match("testing")) + + // Assert case sensitivity followed + assertFalse(simpleMatch.caseSensitive) + assertTrue(simpleMatch.match("@TeSt")) + assertTrue(simpleMatchCS.caseSensitive) + assertFalse(simpleMatchCS.match("@TeSt")) + + // Assert complex match + assertTrue(complexMatch.match("""neverAgonnaBBBgive*you?up\test|yyeah\1\\2\1inval""")) + // Assert complex not literal match + assertFalse(complexMatch.match("""never?gonna*give\*you\?up\\test|y\yeah\\1\\\\2\\\1inval""")) + // Assert complex unrelated not match + assertFalse(complexMatch.match("other")) + } + + @Test + fun matchMultiWildcard() { + val emptyMatch = + ExpressionMatch( + ";!\n;", + ExpressionMatch.MatchMode.MatchMultiWildcard, false + ) + val simpleMatch = + ExpressionMatch( + "?test*;another?", + ExpressionMatch.MatchMode.MatchMultiWildcard, false + ) + val simpleMatchCS = + ExpressionMatch( + "?test*;another?", + ExpressionMatch.MatchMode.MatchMultiWildcard, true + ) + val simpleMatchEscape = + ExpressionMatch( + "\\?test\\*\\\n*thing\\*", + ExpressionMatch.MatchMode.MatchMultiWildcard, false + ) + val simpleMatchInvert = + ExpressionMatch( + """test*;!testing;\!testing""", + ExpressionMatch.MatchMode.MatchMultiWildcard, false + ) + val simpleMatchImplicit = + ExpressionMatch( + """!testing*""", + ExpressionMatch.MatchMode.MatchMultiWildcard, false + ) + val complexMatchFull = """norm;!invert; norm-space ; !invert-space ;;!;\!norm-escaped;""" + + """\\!slash-invert;\\\\double; escape\;sep;slash-end-split\\;""" + + """quad\\\\!noninvert;newline-split""" + "\n" + + """newline-split-slash\\""" + "\n" + + """slash-at-end\\""" + + // Match normal components + val complexMatchNormal = listOf( + """norm""", + """norm-space""", + """!norm-escaped""", + """\!slash-invert""", + """\\double""", + """escape;sep""", + """slash-end-split\""", + """quad\\!noninvert""", + """newline-split""", + """newline-split-slash\""", + """slash-at-end\""" + ) + // Match negating components + val complexMatchInvert = listOf( + """invert""", + """invert-space""" + ) + val complexMatch = ExpressionMatch( + complexMatchFull, ExpressionMatch.MatchMode.MatchMultiWildcard, + false + ) + + // Assert valid and not empty + assertTrue(emptyMatch.isEmpty()) + assertFalse(simpleMatch.isEmpty()) + assertFalse(simpleMatchCS.isEmpty()) + assertFalse(simpleMatchEscape.isEmpty()) + assertFalse(simpleMatchInvert.isEmpty()) + assertFalse(simpleMatchImplicit.isEmpty()) + assertFalse(complexMatch.isEmpty()) + + // Assert match succeeds + assertTrue(simpleMatch.match("@test")) + assertTrue(simpleMatch.match("@testing")) + assertTrue(simpleMatch.match("!test")) + assertTrue(simpleMatch.match("anotherA")) + assertTrue(simpleMatchEscape.match("?test*;thing*")) + assertTrue(simpleMatchEscape.match("?test*;AAAAAthing*")) + assertTrue(simpleMatchInvert.match("test")) + assertTrue(simpleMatchInvert.match("testing things")) + // Assert implicit wildcard succeeds + assertTrue(simpleMatchImplicit.match("AAAAAA")) + // Assert partial match fails + assertFalse(simpleMatch.match("test")) + assertFalse(simpleMatch.match("another")) + assertFalse(simpleMatch.match("anotherBB")) + // Assert unrelated fails + assertFalse(simpleMatch.match("not above")) + // Assert escaped wildcard fails + assertFalse(simpleMatchEscape.match("@testing")) + // Assert inverted match fails + assertFalse(simpleMatchInvert.match("testing")) + assertFalse(simpleMatchImplicit.match("testing")) + + // Assert case sensitivity followed + assertFalse(simpleMatch.caseSensitive) + assertTrue(simpleMatch.match("@TeSt")) + assertTrue(simpleMatchCS.caseSensitive) + assertFalse(simpleMatchCS.match("@TeSt")) + + // Assert complex match + for (normMatch in complexMatchNormal) { + // Each normal component should match + assertTrue(complexMatch.match(normMatch)) + } + + for (invertMatch in complexMatchInvert) { + // Each invert component should not match + assertFalse(complexMatch.match(invertMatch)) + } + + // Assert complex not literal match + assertFalse(complexMatch.match(complexMatchFull)) + // Assert complex unrelated not match + assertFalse(complexMatch.match("other")) + } + + @Test + fun matchRegEx() { + val emptyMatch = + ExpressionMatch( + """ """, + ExpressionMatch.MatchMode.MatchRegEx, false + ) + // Simple regex, case-insensitive + val simpleMatch = + ExpressionMatch( + """simple.\*escape-match.*""", + ExpressionMatch.MatchMode.MatchRegEx, false + ) + // Simple regex, case-sensitive + val simpleMatchCS = + ExpressionMatch( + """simple.\*escape-match.*""", + ExpressionMatch.MatchMode.MatchRegEx, true + ) + // Inverted regex, case-insensitive + val simpleMatchInvert = + ExpressionMatch( + """!invert.\*escape-match.*""", + ExpressionMatch.MatchMode.MatchRegEx, false + ) + // Non-inverted regex, case-insensitive + val simpleMatchNoInvert = + ExpressionMatch( + """\!simple.\*escape-match.*""", + ExpressionMatch.MatchMode.MatchRegEx, false + ) + // Non-inverted regex literal slash, case-insensitive + val simpleMatchNoInvertSlash = + ExpressionMatch( + """\\!simple.\*escape-match.*""", + ExpressionMatch.MatchMode.MatchRegEx, false + ) + + // Assert valid and not empty + assertTrue(emptyMatch.isEmpty()) + assertFalse(simpleMatch.isEmpty()) + assertFalse(simpleMatchCS.isEmpty()) + assertFalse(simpleMatchInvert.isEmpty()) + assertFalse(simpleMatchNoInvert.isEmpty()) + assertFalse(simpleMatchNoInvertSlash.isEmpty()) + + // Assert match succeeds + assertTrue(simpleMatch.match("simpleA*escape-match")) + assertTrue(simpleMatch.match("simpleA*escape-matchBBBB")) + assertTrue(simpleMatchInvert.match("not above")) + assertTrue(simpleMatchNoInvert.match("!simpleA*escape-matchBBBB")) + assertTrue(simpleMatchNoInvertSlash.match("""\!simpleA*escape-matchBBBB""")) + // Assert partial match fails + assertFalse(simpleMatch.match("simpleA*escape-mat")) + assertFalse(simpleMatch.match("simple*escape-match")) + // Assert unrelated fails + assertFalse(simpleMatch.match("not above")) + // Assert escaped wildcard fails + assertFalse(simpleMatch.match("simpleABBBBescape-matchBBBB")) + // Assert inverted fails + assertFalse(simpleMatchInvert.match("invertA*escape-match")) + assertFalse(simpleMatchInvert.match("invertA*escape-matchBBBB")) + assertFalse(simpleMatchNoInvert.match("simpleA*escape-matchBBBB")) + assertFalse(simpleMatchNoInvert.match("anything")) + assertFalse(simpleMatchNoInvertSlash.match("!simpleA*escape-matchBBBB")) + assertFalse(simpleMatchNoInvertSlash.match("anything")) + + // Assert case sensitivity followed + assertFalse(simpleMatch.caseSensitive) + assertTrue(simpleMatch.match("SiMpLEA*escape-MATCH")) + assertTrue(simpleMatchCS.caseSensitive) + assertFalse(simpleMatchCS.match("SiMpLEA*escape-MATCH")) + } + + @Test + fun testInvalid() { + val invalidRegex = ExpressionMatch("*network", ExpressionMatch.MatchMode.MatchRegEx, false) + assertFalse(invalidRegex.match("")) + assertFalse(invalidRegex.match("network")) + assertFalse(invalidRegex.match("testnetwork")) + } + + // Tests imported from https://github.com/ircdocs/parser-tests/blob/master/tests/mask-match.yaml + @Test + fun testDan() { + val mask1 = ExpressionMatch( + "*@127.0.0.1", + ExpressionMatch.MatchMode.MatchWildcard, + caseSensitive = false + ) + println(mask1.positiveRegex) + assertTrue(mask1.match("coolguy!ab@127.0.0.1")) + assertTrue(mask1.match("cooldud3!~bc@127.0.0.1")) + assertFalse(mask1.match("coolguy!ab@127.0.0.5")) + assertFalse(mask1.match("cooldud3!~d@124.0.0.1")) + + val mask2 = ExpressionMatch( + "cool*@*", + ExpressionMatch.MatchMode.MatchWildcard, + caseSensitive = false + ) + println(mask2.positiveRegex) + assertTrue(mask2.match("coolguy!ab@127.0.0.1")) + assertTrue(mask2.match("cooldud3!~bc@127.0.0.1")) + assertTrue(mask2.match("cool132!ab@example.com")) + assertFalse(mask2.match("koolguy!ab@127.0.0.5")) + assertFalse(mask2.match("cooodud3!~d@124.0.0.1")) + + val mask3 = ExpressionMatch( + "cool!*@*", + ExpressionMatch.MatchMode.MatchWildcard, + caseSensitive = false + ) + println(mask3.positiveRegex) + assertTrue(mask3.match("cool!guyab@127.0.0.1")) + assertTrue(mask3.match("cool!~dudebc@127.0.0.1")) + assertTrue(mask3.match("cool!312ab@example.com")) + assertFalse(mask3.match("coolguy!ab@127.0.0.1")) + assertFalse(mask3.match("cooldud3!~bc@127.0.0.1")) + assertFalse(mask3.match("koolguy!ab@127.0.0.5")) + assertFalse(mask3.match("cooodud3!~d@124.0.0.1")) + + // Cause failures in fnmatch/glob based matchers + val mask4 = ExpressionMatch( + "cool[guy]!*@*", + ExpressionMatch.MatchMode.MatchWildcard, + caseSensitive = false + ) + println(mask4.positiveRegex) + assertTrue(mask4.match("cool[guy]!guy@127.0.0.1")) + assertTrue(mask4.match("cool[guy]!a@example.com")) + assertFalse(mask4.match("coolg!ab@127.0.0.1")) + assertFalse(mask4.match("cool[!ac@127.0.1.1")) + } +}