Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix URL.resolve and PathInfo.normalize #2090

Merged
merged 6 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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