fix: uri handling, ci test setup (#4556)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-14 10:07:03 -06:00 committed by GitHub
parent 5061dc8262
commit 0f03492ac6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 115 additions and 34 deletions

View file

@ -20,7 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.model.util.URL_PREFIX
import org.meshtastic.core.model.util.CHANNEL_URL_PREFIX
import org.meshtastic.core.model.util.getChannelUrl
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.proto.ChannelSet
@ -33,7 +33,7 @@ class ChannelTest {
val ch = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig)
val channelUrl = ch.getChannelUrl()
Assert.assertTrue(channelUrl.toString().startsWith(URL_PREFIX))
Assert.assertTrue(channelUrl.toString().startsWith(CHANNEL_URL_PREFIX))
Assert.assertEquals(channelUrl.toChannelSet(), ch)
}

View file

@ -40,21 +40,27 @@ class SharedContactTest {
@Test
fun testWwwHostIsAccepted() {
val url = Uri.parse("https://www.meshtastic.org/v/#CggKBVN1enVtZRICU1oaBTEyMzQ1")
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "www.meshtastic.org")
val url = Uri.parse(urlStr)
val contact = url.toSharedContact()
assertEquals("Suzume", contact.user?.long_name)
}
@Test
fun testLongPathIsAccepted() {
val url = Uri.parse("https://meshtastic.org/contact/v/#CggKBVN1enVtZRICU1oaBTEyMzQ1")
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/contact/v/")
val url = Uri.parse(urlStr)
val contact = url.toSharedContact()
assertEquals("Suzume", contact.user?.long_name)
}
@Test(expected = java.net.MalformedURLException::class)
fun testInvalidHostThrows() {
val url = Uri.parse("https://example.com/v/#CggKBVN1enVtZRICU1oaBTEyMzQ1")
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "example.com")
val url = Uri.parse(urlStr)
url.toSharedContact()
}
}

View file

@ -24,7 +24,7 @@ import org.meshtastic.proto.SharedContact
import org.meshtastic.proto.User
import java.net.MalformedURLException
private const val BASE64FLAGS = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING
private const val BASE64FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
/**
* Return a [SharedContact] that represents the contact encoded by the URL.
@ -33,16 +33,48 @@ private const val BASE64FLAGS = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PAD
*/
@Throws(MalformedURLException::class)
fun Uri.toSharedContact(): SharedContact {
val h = host ?: ""
val isCorrectHost =
h.equals(MESHTASTIC_HOST, ignoreCase = true) || h.equals("www.$MESHTASTIC_HOST", ignoreCase = true)
checkSharedContactUrl()
val data = fragment!!.substringBefore('?')
return decodeSharedContactData(data)
}
@Throws(MalformedURLException::class)
private fun Uri.checkSharedContactUrl() {
val h = host?.lowercase() ?: ""
val isCorrectHost = h == MESHTASTIC_HOST || h == "www.$MESHTASTIC_HOST"
val segments = pathSegments
val isCorrectPath = segments.any { it.equals("v", ignoreCase = true) }
if (fragment.isNullOrBlank() || !isCorrectHost || !isCorrectPath) {
throw MalformedURLException("Not a valid Meshtastic URL")
val frag = fragment
if (frag.isNullOrBlank() || !isCorrectHost || !isCorrectPath) {
throw MalformedURLException(
"Not a valid Meshtastic URL: host=$h, segments=$segments, hasFragment=${!frag.isNullOrBlank()}",
)
}
}
@Throws(MalformedURLException::class)
private fun decodeSharedContactData(data: String): SharedContact {
val decodedBytes =
try {
// We use a more lenient decoding for the input to handle variations from different clients
Base64.decode(data, Base64.DEFAULT or Base64.URL_SAFE)
} catch (e: IllegalArgumentException) {
val ex =
MalformedURLException(
"Failed to Base64 decode SharedContact data ($data): ${e.javaClass.simpleName}: ${e.message}",
)
ex.initCause(e)
throw ex
}
return try {
SharedContact.ADAPTER.decode(decodedBytes.toByteString())
} catch (e: java.io.IOException) {
val ex = MalformedURLException("Failed to proto decode SharedContact: ${e.javaClass.simpleName}: ${e.message}")
ex.initCause(e)
throw ex
}
return SharedContact.ADAPTER.decode(Base64.decode(fragment!!, BASE64FLAGS).toByteString())
}
/** Converts a [SharedContact] to its corresponding URI representation. */

View file

@ -65,4 +65,36 @@ class SharedContactTest {
val url = Uri.parse(urlStr)
url.toSharedContact()
}
@Test(expected = java.net.MalformedURLException::class)
fun testInvalidPathThrows() {
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/wrong/")
val url = Uri.parse(urlStr)
url.toSharedContact()
}
@Test(expected = java.net.MalformedURLException::class)
fun testMissingFragmentThrows() {
val urlStr = "https://meshtastic.org/v/"
val url = Uri.parse(urlStr)
url.toSharedContact()
}
@Test(expected = java.net.MalformedURLException::class)
fun testInvalidBase64Throws() {
val urlStr = "https://meshtastic.org/v/#InvalidBase64!!!!"
val url = Uri.parse(urlStr)
url.toSharedContact()
}
@Test(expected = java.net.MalformedURLException::class)
fun testInvalidProtoThrows() {
// Tag 0 is invalid in Protobuf
// 0x00 -> Tag 0, Type 0.
// Base64 for 0x00 is "AA=="
val urlStr = "https://meshtastic.org/v/#AA=="
val url = Uri.parse(urlStr)
url.toSharedContact()
}
}