Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c444be48 authored by cketti's avatar cketti
Browse files

Add a `UriParser` for generic URIs

This isn't actually a proper parser for the URI generic syntax. But it is good enough to linkify URIs.
parent 84853952
Loading
Loading
Loading
Loading
+34 −0
Original line number Diff line number Diff line
package com.fsck.k9.message.html

import java.util.regex.Pattern

/**
 * Matches the URI generic syntax.
 *
 * See [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986).
 */
class GenericUriParser : UriParser {
    override fun parseUri(text: CharSequence, startPos: Int): UriMatch? {
        val matcher = PATTERN.matcher(text)
        if (!matcher.find(startPos) || matcher.start() != startPos) return null

        val startIndex = matcher.start()
        val endIndex = matcher.end()
        val uri = text.subSequence(startIndex, endIndex)

        return UriMatch(startIndex, endIndex, uri)
    }

    companion object {
        private const val SCHEME = "[a-zA-Z][a-zA-Z0-9+.\\-]*"
        private const val AUTHORITY = "[a-zA-Z0-9\\-._~%!\$&'()*+,;=:\\[\\]@]*"
        private const val PATH = "[a-zA-Z0-9\\-._~%!\$&'()*+,;=:@/]*"
        private const val QUERY = "[a-zA-Z0-9\\-._~%!\$&'()*+,;=:@/?]*"
        private const val FRAGMENT = "[a-zA-Z0-9\\-._~%!\$&'()*+,;=:@/?]*"

        // This regular expression matches more than allowed by the generic URI syntax. So we might end up linkifying
        // text that is not a proper URI. We leave apps actually handling the URI when the user clicks on such a link
        // to deal with this case.
        private val PATTERN = Pattern.compile("$SCHEME:(?://$AUTHORITY)?(?:$PATH)?(?:\\?$QUERY)?(?:#$FRAGMENT)?")
    }
}
+5 −1
Original line number Diff line number Diff line
@@ -3,10 +3,14 @@ package com.fsck.k9.message.html
object UriMatcher {
    private val SUPPORTED_URIS = run {
        val httpUriParser = HttpUriParser()
        val genericUriParser = GenericUriParser()
        mapOf(
            "http:" to httpUriParser,
            "https:" to httpUriParser,
            "rtsp:" to httpUriParser
            "mailto:" to genericUriParser,
            "matrix:" to genericUriParser,
            "rtsp:" to httpUriParser,
            "xmpp:" to genericUriParser,
        )
    }

+71 −0
Original line number Diff line number Diff line
package com.fsck.k9.message.html

import com.google.common.truth.Truth.assertThat
import kotlin.test.assertNotNull
import org.junit.Test

class GenericUriParserTest {
    private val parser = GenericUriParser()

    @Test
    fun `mailto URIs`() {
        // Examples from RFC 6068
        assertUriValid("mailto:chris@example.com")
        assertUriValid("mailto:infobot@example.com?subject=current-issue")
        assertUriValid("mailto:infobot@example.com?body=send%20current-issue")
        assertUriValid("mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index")
        assertUriValid("mailto:list@example.org?In-Reply-To=%3C3469A91.D10AF4C@example.com%3E")
        assertUriValid("mailto:majordomo@example.com?body=subscribe%20bamboo-l")
        assertUriValid("mailto:joe@example.com?cc=bob@example.com&body=hello")
        assertUriValid("mailto:gorby%25kremvax@example.com")
        assertUriValid("mailto:unlikely%3Faddress@example.com?blat=foop")
        assertUriValid("mailto:%22not%40me%22@example.org")
        assertUriValid("mailto:%22oh%5C%5Cno%22@example.org")
        assertUriValid("mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org")
        assertUriValid("mailto:user@example.org?subject=caf%C3%A9")
        assertUriValid("mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D")
        assertUriValid("mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D")
        assertUriValid("mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9")
        assertUriValid("mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO")
    }

    @Test
    fun `XMPP URIs`() {
        // Examples from RFC 5122
        assertUriValid("xmpp:node@example.com")
        assertUriValid("xmpp://guest@example.com")
        assertUriValid("xmpp://guest@example.com/support@example.com?message")
        assertUriValid("xmpp:support@example.com?message")
        assertUriValid("xmpp:example-node@example.com/some-resource")
        assertUriValid("xmpp:example.com")
        assertUriValid("xmpp:example-node@example.com?message")
        assertUriValid("xmpp:example-node@example.com?message;subject=Hello%20World")
        assertUriValid("xmpp:nasty!%23\$%25()*+,-.;=%3F%5B%5C%5D%5E_%60%7B%7C%7D~node@example.com")
        assertUriValid("xmpp:node@example.com/repulsive%20!%23%22\$%25&'()*+,-.%2F:;%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~resource")
        assertUriValid("xmpp:ji%C5%99i@%C4%8Dechy.example/v%20Praze")
    }

    @Test
    fun `matrix URIs`() {
        // Examples from MSC 2312
        assertUriValid("matrix:r/someroom:example.org")
        assertUriValid("matrix:u/me:example.org")
        assertUriValid("matrix:r/someroom:example.org/e/Arbitrary_Event_Id")
        assertUriValid("matrix:u/her:example.org")
        assertUriValid("matrix:u/her:example.org?action=chat")
        assertUriValid("matrix:roomid/rid:example.org")
        assertUriValid("matrix:r/us:example.org")
        assertUriValid("matrix:roomid/rid:example.org?action=join&via=example2.org")
        assertUriValid("matrix:r/us:example.org?action=join")
        assertUriValid("matrix:r/us:example.org/e/lol823y4bcp3qo4")
        assertUriValid("matrix:roomid/rid:example.org/event/lol823y4bcp3qo4?via=example2.org")
    }

    private fun assertUriValid(input: String) {
        val result = parser.parseUri(input, 0)

        assertNotNull(result) { uriMatch ->
            assertThat(uriMatch.uri).isEqualTo(input)
        }
    }
}