Skip to content

Commit

Permalink
fix URL.resolve and PathInfo.normalize (#2090)
Browse files Browse the repository at this point in the history
  • Loading branch information
itboy87 committed Dec 29, 2023
1 parent cc5c2f3 commit 1ffa0dc
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 23 deletions.
40 changes: 21 additions & 19 deletions korge-core/src/korlibs/io/file/PathInfo.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package korlibs.io.file

import korlibs.datastructure.count
import korlibs.datastructure.iterators.fastForEach
import korlibs.datastructure.iterators.*
import korlibs.io.lang.indexOfOrNull
import korlibs.io.lang.lastIndexOfOrNull
import korlibs.io.net.MimeType
Expand Down Expand Up @@ -185,24 +185,26 @@ open class VfsNamed(override val pathInfo: PathInfo) : Path


fun PathInfo.parts(): List<String> = fullPath.split('/')
fun PathInfo.normalize(): String {
val path = this.fullPath
val schemeIndex = path.indexOf(":")
return if (schemeIndex >= 0) {
val take = if (path.substring(schemeIndex).startsWith("://")) 3 else 1
path.substring(0, schemeIndex + take) + path.substring(schemeIndex + take).pathInfo.normalize()
} else {
val path2 = path.replace('\\', '/')
val out = ArrayList<String>()
path2.split("/").fastForEach { part ->
when (part) {
"", "." -> if (out.isEmpty()) out += "" else Unit
".." -> if (out.isNotEmpty()) out.removeAt(out.size - 1)
else -> out += part
}
}
out.joinToString("/")
}
fun PathInfo.normalize(removeEndSlash: Boolean = true): String {
val path = this.fullPath
val schemeIndex = path.indexOf(":")
return if (schemeIndex >= 0) {
val take = if (path.substring(schemeIndex).startsWith("://")) 3 else 1
path.substring(0, schemeIndex + take) + path.substring(schemeIndex + take).pathInfo.normalize(removeEndSlash = removeEndSlash)
} else {
val path2 = path.replace('\\', '/')
val out = ArrayList<String>()
val path2PathLength: Int
path2.split("/").also { path2PathLength = it.size }.fastForEachWithIndex { index, part ->
when (part) {
"" -> if (out.isEmpty() || !removeEndSlash) out += ""
"." -> if (index == path2PathLength - 1 && !removeEndSlash) out += ""
".." -> if (out.isNotEmpty() && index != 1) out.removeAt(out.size - 1)
else -> out += part
}
}
out.joinToString("/")
}
}

fun PathInfo.combine(access: PathInfo): PathInfo {
Expand Down
29 changes: 25 additions & 4 deletions korge-core/src/korlibs/io/net/URL.kt
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,22 @@ data class URL private constructor(

fun isAbsolute(url: String): Boolean = StrReader(url).tryRegex(schemeRegex) != null

fun resolve(base: String, access: String): String = when {
isAbsolute(access) -> access
access.startsWith("/") -> URL(base).copy(path = access).fullUrl
else -> URL(base).run { copy(path = "/${("${path.substringBeforeLast('/')}/$access").pathInfo.normalize().trimStart('/')}").fullUrl }
fun resolve(base: String, access: String): String {
// if access url is relative protocol then copy it from base
val refinedAccess = if (access.startsWith("//")) "${base.substringBefore(":")}:$access" else access
return when {
refinedAccess.isEmpty() -> base
isAbsolute(refinedAccess) -> refinedAccess
refinedAccess.startsWith("/") -> URL(base).copy(path = refinedAccess.normalizeUrl(), query = null).fullUrl
else -> URL(base).run {
val refinedPath = if(refinedAccess.startsWith("?") || refinedAccess.startsWith("#")) {
"${path}$refinedAccess"
} else {
"${path.substringBeforeLast('/')}/$refinedAccess"
}
copy(path = "/${refinedPath.normalizeUrl().trimStart('/')}", query = null).fullUrl
}
}
}

fun decodeComponent(s: String, charset: Charset = UTF8, formUrlEncoded: Boolean = false): String {
Expand Down Expand Up @@ -202,3 +214,12 @@ data class URL private constructor(
fun createBase64URLForData(data: ByteArray, contentType: String): String {
return "data:$contentType;base64,${data.toBase64()}"
}

fun String.normalizeUrl(): String {
// Split with the query string or fragment, whichever comes first,
// to avoid normalizing query string and fragment
val paramFlag = this.find { it == '?' || it == '#' } ?: '?'
val pathParts = this.split(paramFlag).toMutableList()
pathParts[0] = pathParts[0].pathInfo.normalize(removeEndSlash = false)
return pathParts.joinToString(paramFlag.toString())
}
58 changes: 58 additions & 0 deletions korge-core/test/korlibs/io/net/URLTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package korlibs.io.net

import korlibs.io.file.*
import korlibs.io.lang.toByteArray
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -100,6 +101,22 @@ class URLTest {
for (url in URLS) assertEquals(url.isOpaque, URL(url.url).isOpaque, url.url)
}

@Test
fun testNormalize() {
assertEquals("g", "./g/.".pathInfo.normalize())
assertEquals("g", "././g".pathInfo.normalize())
assertEquals("g", "./g/.".pathInfo.normalize())
assertEquals("g", "g/".pathInfo.normalize())
assertEquals("/g", "/./g".pathInfo.normalize())
assertEquals("/g", "/../g".pathInfo.normalize())
assertEquals("g", "./g".pathInfo.normalize())
assertEquals("g", "g/.".pathInfo.normalize())
assertEquals("g/", "g/.".normalizeUrl())
assertEquals("g/", "g/".normalizeUrl())
assertEquals("g/", "./g/.".normalizeUrl())
assertEquals("g/", "./g/.".normalizeUrl())
}

@Test
fun testResolve() {
assertEquals("https://www.google.es/", URL.resolve("https://google.es/", "https://www.google.es/"))
Expand All @@ -109,6 +126,47 @@ class URLTest {
assertEquals("https://google.es/test", URL.resolve("https://google.es/path/path2", "../test"))
assertEquals("https://google.es/path/test", URL.resolve("https://google.es/path/path2/", "../test"))
assertEquals("https://google.es/test", URL.resolve("https://google.es/path/path2/", "../../../test"))

assertEquals("http://example.com/one/two?three", URL.resolve("http://example.com", "./one/two?three"))
assertEquals("http://example.com/one/two?three", URL.resolve("http://example.com?one", "./one/two?three"))
assertEquals("http://example.com/one/two?three#four", URL.resolve("http://example.com", "./one/two?three#four"))
assertEquals("https://example.com/one", URL.resolve("http://example.com/", "https://example.com/one"))
assertEquals("http://example.com/one/two.html", URL.resolve("http://example.com/two/", "../one/two.html"))
assertEquals("https://example2.com/one", URL.resolve("https://example.com/", "//example2.com/one"))
assertEquals("https://example.com:8080/one", URL.resolve("https://example.com:8080", "./one"))
assertEquals("https://example2.com/one", URL.resolve("http://example.com/", "https://example2.com/one"))
assertEquals("https://example.com/one", URL.resolve("wrong", "https://example.com/one"))
assertEquals("https://example.com/one", URL.resolve("https://example.com/one", ""))
assertEquals("https://example.com/one/two.c", URL.resolve("https://example.com/one/two/", "../two.c"))
assertEquals("https://example.com/two.c", URL.resolve("https://example.com/one/two", "../two.c"))
assertEquals("ftp://example.com/one", URL.resolve("ftp://example.com/two/", "../one"))
assertEquals("ftp://example.com/one/two.c", URL.resolve("ftp://example.com/one/", "./two.c"))
assertEquals("ftp://example.com/one/two.c", URL.resolve("ftp://example.com/one/", "two.c"))
// examples taken from rfc3986 section 5.4.2
assertEquals("http://example.com/g", URL.resolve("http://example.com/b/c/d;p?q", "../../../g"))
assertEquals("http://example.com/g", URL.resolve("http://example.com/b/c/d;p?q", "../../../../g"))
assertEquals("http://example.com/g", URL.resolve("http://example.com/b/c/d;p?q", "/./g"))
assertEquals("http://example.com/g", URL.resolve("http://example.com/b/c/d;p?q", "/../g"))
assertEquals("http://example.com/b/c/g.", URL.resolve("http://example.com/b/c/d;p?q", "g."))
assertEquals("http://example.com/b/c/.g", URL.resolve("http://example.com/b/c/d;p?q", ".g"))
assertEquals("http://example.com/b/c/g..", URL.resolve("http://example.com/b/c/d;p?q", "g.."))
assertEquals("http://example.com/b/c/..g", URL.resolve("http://example.com/b/c/d;p?q", "..g"))
assertEquals("http://example.com/b/g", URL.resolve("http://example.com/b/c/d;p?q", "./../g"))
assertEquals("http://example.com/b/c/g/", URL.resolve("http://example.com/b/c/d;p?q", "./g/."))
assertEquals("http://example.com/b/c/g/h", URL.resolve("http://example.com/b/c/d;p?q", "g/./h"))
assertEquals("http://example.com/b/c/h", URL.resolve("http://example.com/b/c/d;p?q", "g/../h"))
assertEquals("http://example.com/b/c/g;x=1/y", URL.resolve("http://example.com/b/c/d;p?q", "g;x=1/./y"))
assertEquals("http://example.com/b/c/y", URL.resolve("http://example.com/b/c/d;p?q", "g;x=1/../y"))
assertEquals("http://example.com/b/c/g?y/./x", URL.resolve("http://example.com/b/c/d;p?q", "g?y/./x"))
assertEquals("http://example.com/b/c/g?y/../x", URL.resolve("http://example.com/b/c/d;p?q", "g?y/../x"))
assertEquals("http://example.com/b/c/g#s/./x", URL.resolve("http://example.com/b/c/d;p?q", "g#s/./x"))
assertEquals("http://example.com/b/c/g#s/../x", URL.resolve("http://example.com/b/c/d;p?q", "g#s/../x"))
assertEquals("https://example.com/path/bar.html?foo", URL.resolve("https://example.com/path/file?bar", "bar.html?foo"))
assertEquals("https://example.com/path/file?foo", URL.resolve("https://example.com/path/file?bar", "?foo"))
assertEquals("https://example.com/foo bar/", URL.resolve("https://example.com/example/", "../foo bar/"))
assertEquals("file:///var/log/messages", URL.resolve("file:///etc/", "/var/log/messages"))
assertEquals("file:///var/log/messages", URL.resolve("file:///etc", "/var/log/messages"))
assertEquals("file:///etc/var/log/messages", URL.resolve("file:///etc/", "var/log/messages"))
}

@Test
Expand Down

0 comments on commit 1ffa0dc

Please sign in to comment.