From 36ac57de89bd7fbca08fbf25ef7201eb5bfe9287 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sun, 18 Oct 2026 12:09:23 +0200 Subject: [PATCH] =?utf8?q?=F0=9F=90=9B=20Decode=20URI=20after=20toString()?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Because toString() encodes some characters again, like whitespace. --- .../sone/core/DefaultElementLoader.kt | 16 ++-- .../sone/core/DefaultElementLoaderTest.kt | 95 +++++++++++++++++++--- 2 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt index 1f3130b..e2d351a 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt @@ -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()) } } } diff --git a/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt index 58ad8fd..e600db7 100644 --- a/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt @@ -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 + }) + } } -- 2.7.4