🐛 Decode URI after toString()
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 18 Oct 2026 10:09:23 +0000 (12:09 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 18 Oct 2026 10:09:23 +0000 (12:09 +0200)
Because toString() encodes some characters again, like whitespace.

src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt
src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt

index 1f3130b..e2d351a 100644 (file)
@@ -32,12 +32,12 @@ class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticke
                override fun shouldCancel(uri: FreenetURI, mimeType: String, size: Long): Boolean {
                        if (size > 2097152) {
                                logger.fine { "Canceling download of $uri because it’s > 2 MiB." }
-                               rejectedElements[uri.toString()] = Rejected(RejectionReason.SizeTooLarge, size)
+                               rejectedElements[uri.toString().decode()] = Rejected(RejectionReason.SizeTooLarge, size)
                                return true
                        }
                        if (!mimeType.startsWith("image/") && !mimeType.startsWith("text/html")) {
                                logger.fine { "Canceling download of $uri because of its MIME type, $mimeType." }
-                               rejectedElements[uri.toString()] = Rejected(RejectionReason.InvalidMediaType, mimeType)
+                               rejectedElements[uri.toString().decode()] = Rejected(RejectionReason.InvalidMediaType, mimeType)
                                return true
                        }
                        return false
@@ -50,8 +50,8 @@ class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticke
                                                ByteArrayInputStream(data).use {
                                                        ImageIO.read(it)
                                                }?.let {
-                                                       elementCache.get(uri.toString()) {
-                                                               LinkedElement(uri.toString(), properties = mapOf("type" to "image", "size" to data.size, "sizeHuman" to data.size.human))
+                                                       elementCache.get(uri.toString().decode()) {
+                                                               LinkedElement(uri.toString().decode(), properties = mapOf("type" to "image", "size" to data.size, "sizeHuman" to data.size.human))
                                                                        .apply {
                                                                                logger.fine("Downloaded image from $link: size=${properties["size"]}.")
                                                                        }
@@ -60,8 +60,8 @@ class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticke
                                        }
                                        mimeType.baseType == "text/html" -> {
                                                val document = Jsoup.parse(data.toString(Charset.forName(mimeType.getParameter("charset") ?: "UTF-8")))
-                                               elementCache.get(uri.toString()) {
-                                                       LinkedElement(uri.toString(), properties = mapOf(
+                                               elementCache.get(uri.toString().decode()) {
+                                                       LinkedElement(uri.toString().decode(), properties = mapOf(
                                                                        "type" to "html", "size" to data.size, "sizeHuman" to data.size.human,
                                                                        "title" to document.title().emptyToNull,
                                                                        "description" to (document.metaDescription ?: document.firstNonHeadingParagraph)
@@ -76,14 +76,14 @@ class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticke
                }
 
                override fun failed(uri: FreenetURI) {
-                       failureCache.put(uri.toString(), true)
+                       failureCache.put(uri.toString().decode(), true)
                        removeLoadingLink(uri)
                        logger.fine { "Download failed for $uri." }
                }
 
                private fun removeLoadingLink(uri: FreenetURI) {
                        synchronized(loadingLinks) {
-                               loadingLinks.invalidate(uri.toString())
+                               loadingLinks.invalidate(uri.toString().decode())
                        }
                }
        }
index 58ad8fd..e600db7 100644 (file)
@@ -28,6 +28,7 @@ import org.hamcrest.Matchers.containsString
 import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.hasEntry
 import org.hamcrest.Matchers.hasItem
+import org.hamcrest.Matchers.hasKey
 import org.hamcrest.Matchers.hasSize
 import org.hamcrest.Matchers.not
 import org.hamcrest.TypeSafeDiagnosingMatcher
@@ -271,13 +272,28 @@ class DefaultElementLoaderTest {
        @Test
        fun `element loader no longer exposes an element as loading once it has been loaded`() {
                runWithCallback("KSK@test") { elementLoader, _, callback, _ ->
-                       elementLoader.loadElement("KSK@test")
                        callback.loaded(FreenetURI("KSK@test"), "image/png", read("/static/images/unknown-image-0.png"))
                        assertThat(elementLoader.loadingElements, not(hasItem("KSK@test")))
                }
        }
 
        @Test
+       fun `element loader no longer exposes an element as loading once it was loaded as text`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, _ ->
+                       callback.loaded(FreenetURI("KSK@test%20file"), "text/html", ByteArray(0))
+                       assertThat(elementLoader.loadingElements, not(hasItem("KSK@test file")))
+               }
+       }
+
+       @Test
+       fun `element loader no longer exposes an element as loading once it was loaded as image`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, _ ->
+                       callback.loaded(FreenetURI("KSK@test%20file"), "image/png", read("/static/images/unknown-image-0.png"))
+                       assertThat(elementLoader.loadingElements, not(hasItem("KSK@test file")))
+               }
+       }
+
+       @Test
        fun `element loader exposes rejected element when media type does not match`() {
                runWithCallback("KSK@test") { elementLoader, _, callback, _ ->
                        callback.shouldCancel(FreenetURI("KSK@test"), "application/x-test-data", 1234)
@@ -311,12 +327,67 @@ class DefaultElementLoaderTest {
        }
 
        @Test
+       fun `element loader no longer exposes an element as loading once it has failed`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, _ ->
+                       callback.failed(FreenetURI("KSK@test%20file"))
+                       assertThat(elementLoader.loadingElements, not(hasItem("KSK@test file")))
+               }
+       }
+
+       @Test
        fun `element loader decodes and normalizes link`() {
                runWithCallback(decomposedKey) { _, _, _, fetchedUris ->
                        assertThat(fetchedUris(), contains(FreenetURI(normalizedKey)))
                }
        }
 
+       @Test
+       fun `url of element rejected because of size is decoded`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, fetchedUris ->
+                       callback.shouldCancel(FreenetURI("KSK@test%20file"), "text/html", Long.MAX_VALUE)
+                       assertThat(elementLoader.rejectedElements, hasKey("KSK@test file"))
+               }
+       }
+
+       @Test
+       fun `url of element rejected because of MIME type is decoded`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, fetchedUris ->
+                       callback.shouldCancel(FreenetURI("KSK@test%20file"), "test/data", 1L)
+                       assertThat(elementLoader.rejectedElements, hasKey("KSK@test file"))
+               }
+       }
+
+       @Test
+       fun `url of element that failed to load is decoded`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, fetchedUris ->
+                       callback.failed(FreenetURI("KSK@test%20file"))
+                       assertThat(elementLoader.failedElements, containsInAnyOrder("KSK@test file"))
+               }
+       }
+
+       @Test
+       fun `url of loading element is decoded`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, fetchedUris ->
+                       assertThat(elementLoader.loadingElements, containsInAnyOrder("KSK@test file"))
+               }
+       }
+
+       @Test
+       fun `url of loaded text element is decoded`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, fetchedUris ->
+                       callback.loaded(FreenetURI("KSK@test%20file"), "text/html", ByteArray(0))
+                       assertThat(elementLoader.loadedElements, containsInAnyOrder(isLinkedElement(equalTo("KSK@test file"))))
+               }
+       }
+
+       @Test
+       fun `url of loaded image element is decoded`() {
+               runWithCallback("KSK@test%20file") { elementLoader, _, callback, fetchedUris ->
+                       callback.loaded(FreenetURI("KSK@test%20file"), "image/test", read("/static/images/unknown-image-0.png"))
+                       assertThat(elementLoader.loadedElements, containsInAnyOrder(isLinkedElement(equalTo("KSK@test file"))))
+               }
+       }
+
        private fun read(resource: String): ByteArray =
                javaClass.getResourceAsStream(resource)?.use { input ->
                        ByteArrayOutputStream().use {
@@ -333,17 +404,17 @@ class DefaultElementLoaderTest {
        @Suppress("unused")
        // keep this instance around so its configuration isn’t lost
        private val logger = Logger.getLogger(DefaultElementLoader::class.qualifiedName)
-       .apply { level = ALL }
-       .apply {
-               addHandler(object : Handler() {
-                       override fun publish(record: LogRecord) {
-                               loggedRecords += record
-                       }
-
-                       override fun flush() = Unit
-                       override fun close() = Unit
-               })
-       }
+               .apply { level = ALL }
+               .apply {
+                       addHandler(object : Handler() {
+                               override fun publish(record: LogRecord) {
+                                       loggedRecords += record
+                               }
+
+                               override fun flush() = Unit
+                               override fun close() = Unit
+                       })
+               }
 
 }