Open the user profile of a user id that contains a slash

A slash is valid in the local part of a user id, but in a permalink it
also separates the identifier from an event id, so the SDK could not tell
the two apart and the link fell back to opening matrix.to in a browser.

Only a user id can start with an @, so recover it before handing the URI
to the SDK, and only when the identifier really contains a slash so every
other permalink keeps taking the same path.
This commit is contained in:
hayaksi1
2026-08-17 03:48:44 +03:00
parent 23b75c3423
commit c5a878249b
2 changed files with 64 additions and 0 deletions
@@ -8,11 +8,13 @@
package io.element.android.libraries.matrix.impl.permalink package io.element.android.libraries.matrix.impl.permalink
import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@@ -51,6 +53,10 @@ class DefaultPermalinkParser(
matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri)
} }
matrixToUri.userIdWithSlash()?.let { userId ->
return PermalinkData.UserLink(userId = userId)
}
val result = runCatchingExceptions { val result = runCatchingExceptions {
parseMatrixEntityFrom(matrixToUri.toString()) parseMatrixEntityFrom(matrixToUri.toString())
}.getOrNull() }.getOrNull()
@@ -84,3 +90,17 @@ class DefaultPermalinkParser(
} }
} }
} }
/**
* A slash is allowed in the local part of a user id, but it also separates an identifier from an
* event id in a permalink, so [parseMatrixEntityFrom] cannot tell the two apart. Only a user id can
* start with an `@`, so recover it here rather than letting the ambiguity reach the SDK.
*/
private fun Uri.userIdWithSlash(): UserId? {
val identifier = fragment
?.removePrefix("/")
?.substringBefore('?')
?: return null
if (!identifier.startsWith("@") || !identifier.contains('/')) return null
return identifier.takeIf { MatrixPatterns.isUserId(it) }?.let(::UserId)
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.permalink
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test
class DefaultPermalinkParserTest : RobolectricTest() {
@Test
fun `a matrix-to link to a user id with a slash is parsed as a user link`() {
val result = createParser().parse("https://matrix.to/#/@it/sme:matrix.org")
assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org")))
}
@Test
fun `an escaped slash in a user id is parsed as a user link`() {
val result = createParser().parse("https://matrix.to/#/@it%2Fsme:matrix.org")
assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org")))
}
@Test
fun `an element web link to a user id with a slash is parsed as a user link`() {
val result = createParser().parse("https://app.element.io/#/user/@it/sme:matrix.org")
assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org")))
}
@Test
fun `a user id with a slash that is not a valid user id is not a user link`() {
val result = createParser().parse("https://example.org/foo/@it/sme")
assertThat(result).isInstanceOf(PermalinkData.FallbackLink::class.java)
}
private fun createParser() = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
}