UriUtils: add a bunch of URI utilities

So we can now click on relative links, wow! Fast progress!
This commit is contained in:
dece 2021-12-09 01:44:52 +01:00
parent e540cf7628
commit 57854e56dc
6 changed files with 173 additions and 41 deletions

View file

@ -18,6 +18,9 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
testOptions {
unitTests.returnDefaultValues = true
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false

View file

@ -1,24 +0,0 @@
package dev.lowrespalmtree.comet
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("dev.lowrespalmtree.comet", appContext.packageName)
}
}

View file

@ -0,0 +1,76 @@
package dev.lowrespalmtree.comet
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class UriUtilsTest {
@Test
fun joinUrls() {
assertEquals(
"gemini://dece.space/some-file.gmi",
joinUrls("gemini://dece.space/", "some-file.gmi").toString()
)
assertEquals(
"gemini://dece.space/some-file.gmi",
joinUrls("gemini://dece.space/", "./some-file.gmi").toString()
)
assertEquals(
"gemini://dece.space/some-file.gmi",
joinUrls("gemini://dece.space/dir1", "/some-file.gmi").toString()
)
assertEquals(
"gemini://dece.space/dir1/other-file.gmi",
joinUrls("gemini://dece.space/dir1/file.gmi", "other-file.gmi").toString()
)
assertEquals(
"gemini://dece.space/top-level.gmi",
joinUrls("gemini://dece.space/dir1/file.gmi", "../top-level.gmi").toString()
)
assertEquals(
"s://hard/test/b/d/a.gmi",
joinUrls("s://hard/dir/a", "./../test/b/c/../d/e/f/../.././a.gmi").toString()
)
}
@Test
fun removeDotSegments() {
arrayOf(
Pair("index.gmi", "index.gmi"),
Pair("/index.gmi", "/index.gmi"),
Pair("./index.gmi", "index.gmi"),
Pair("/./index.gmi", "/index.gmi"),
Pair("/../index.gmi", "/index.gmi"),
Pair("/a/b/c/./../../g", "/a/g"),
Pair("mid/content=5/../6", "mid/6"),
Pair("../../../../g", "g")
).forEach { (path, expected) ->
assertEquals(expected, removeDotSegments(path))
}
}
@Test
fun removeLastSegment() {
assertEquals("", removeLastSegment(""))
assertEquals("", removeLastSegment("/"))
assertEquals("", removeLastSegment("/a"))
assertEquals("/a", removeLastSegment("/a/"))
assertEquals("/a", removeLastSegment("/a/b"))
assertEquals("/a/b/c", removeLastSegment("/a/b/c/d"))
assertEquals("//", removeLastSegment("///"))
}
@Test
fun popFirstSegment() {
assertEquals(Pair("", ""), popFirstSegment(""))
assertEquals(Pair("a", ""), popFirstSegment("a"))
assertEquals(Pair("/a", ""), popFirstSegment("/a"))
assertEquals(Pair("/a", "/"), popFirstSegment("/a/"))
assertEquals(Pair("/a", "/b"), popFirstSegment("/a/b"))
assertEquals(Pair("a", "/b"), popFirstSegment("a/b"))
}
}

View file

@ -61,6 +61,12 @@ class MainActivity : AppCompatActivity(), ContentAdapter.ContentAdapterListen {
private fun openUrl(url: String, base: String? = null) { private fun openUrl(url: String, base: String? = null) {
var uri = Uri.parse(url) var uri = Uri.parse(url)
if (!uri.isAbsolute) {
uri = if (!base.isNullOrEmpty()) joinUrls(base, url) else toGeminiUri(uri)
Log.d(TAG, "openUrl: '$url' - '$base' - '$uri'")
Log.d(TAG, "openUrl: ${uri.authority} - ${uri.path} - ${uri.query}")
binding.addressBar.setText(uri.toString())
}
when (uri.scheme) { when (uri.scheme) {
"gemini" -> pageViewModel.sendGeminiRequest(uri) "gemini" -> pageViewModel.sendGeminiRequest(uri)

View file

@ -0,0 +1,88 @@
package dev.lowrespalmtree.comet
import android.net.Uri
import android.util.Log
/**
* Transform a relative URI to an absolute Gemini URI
*
* This is mostly to translate user-friendly URLs such as "medusae.space" into
* "gemini://medusae.space". This assumes that the Uri parsing put what the user probably intended
* as the hostname into the path field instead of the authority. Thus, it will NOT merely translate
* any absolute URI into a gemini-scheme URI.
*/
fun toGeminiUri(uri: Uri): Uri =
Uri.Builder()
.scheme("gemini")
.authority(uri.path)
.query(uri.query)
.fragment(uri.fragment)
.build()
/** Return the URI obtained from considering the relative part of an URI wrt/ a base URI. */
fun joinUrls(base: String, relative: String): Uri {
val baseUri = Uri.parse(base)
val relUri = Uri.parse(relative)
val newPath = removeDotSegments(
if (relative.startsWith("/")) {
relUri.path ?: ""
} else {
removeLastSegment(
baseUri.path ?: ""
) + "/" +
(relUri.path ?: "")
}
)
return Uri.Builder()
.scheme(baseUri.scheme)
.authority(baseUri.authority)
.path(newPath)
.query(relUri.query)
.fragment(relUri.fragment)
.build()
}
/** Remove all the sneaky dot segments from the path. */
internal fun removeDotSegments(path: String): String {
var output = ""
var slice = path
while (slice.isNotEmpty()) {
if (slice.startsWith("../")) {
slice = slice.substring(3)
} else if (slice.startsWith("./") || slice.startsWith("/./")) {
slice = slice.substring(2)
} else if (slice == "/.") {
slice = "/"
} else if (slice.startsWith("/../")) {
slice = "/" + slice.substring(4)
output = removeLastSegment(output)
} else if (slice == "/..") {
slice = "/"
output = removeLastSegment(output)
} else if (slice == "." || slice == "..") {
slice = ""
} else {
val (firstSegment, remaining) = popFirstSegment(slice)
output += firstSegment
slice = remaining
}
}
return output
}
/** Remove the last segment of that path, including preceding "/" if any. */
internal fun removeLastSegment(path: String): String {
val lastSlashIndex = path.indexOfLast { it == '/' }
return if (lastSlashIndex > -1) path.slice(0 until lastSlashIndex) else path
}
/** Return first segment and the rest. */
internal fun popFirstSegment(path: String): Pair<String, String> {
if (path.isEmpty())
return Pair(path, "")
var nextSlash = path.substring(1).indexOf("/")
if (nextSlash == -1)
return Pair(path, "")
nextSlash++
return Pair(path.substring(0, nextSlash), path.substring(nextSlash))
}

View file

@ -1,17 +0,0 @@
package dev.lowrespalmtree.comet
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}