diff --git a/korge-core/src/korlibs/io/file/PathInfo.kt b/korge-core/src/korlibs/io/file/PathInfo.kt index 47088b3abf..39640fd1f1 100644 --- a/korge-core/src/korlibs/io/file/PathInfo.kt +++ b/korge-core/src/korlibs/io/file/PathInfo.kt @@ -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 @@ -185,24 +185,26 @@ open class VfsNamed(override val pathInfo: PathInfo) : Path fun PathInfo.parts(): List = 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() - 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() + 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 { diff --git a/korge-core/src/korlibs/io/net/URL.kt b/korge-core/src/korlibs/io/net/URL.kt index 4b62683cd6..1bc5e95b05 100644 --- a/korge-core/src/korlibs/io/net/URL.kt +++ b/korge-core/src/korlibs/io/net/URL.kt @@ -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 { @@ -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()) +} diff --git a/korge-core/test/korlibs/io/net/URLTest.kt b/korge-core/test/korlibs/io/net/URLTest.kt index 669cd13b1e..c4f8452f73 100644 --- a/korge-core/test/korlibs/io/net/URLTest.kt +++ b/korge-core/test/korlibs/io/net/URLTest.kt @@ -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 @@ -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/")) @@ -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