diff --git a/korge-core/src/korlibs/io/net/URL.kt b/korge-core/src/korlibs/io/net/URL.kt index a05da44c0..235e254c7 100644 --- a/korge-core/src/korlibs/io/net/URL.kt +++ b/korge-core/src/korlibs/io/net/URL.kt @@ -13,6 +13,7 @@ import korlibs.io.lang.* data class URL private constructor( val isOpaque: Boolean, val scheme: String?, + val subScheme: String?, val userInfo: String?, val host: String?, val path: String, @@ -43,6 +44,7 @@ data class URL private constructor( fun toUrlString(includeScheme: Boolean = true, out: StringBuilder = StringBuilder()): StringBuilder { if (includeScheme && scheme != null) { out.append("$scheme:") + if (subScheme != null) out.append("$subScheme:") if (!isOpaque) out.append("//") } if (userInfo != null) out.append("$userInfo@") @@ -58,7 +60,7 @@ data class URL private constructor( override fun toString(): String = fullUrl fun toComponentString(): String { - return "URL(" + listOf(::scheme, ::userInfo, ::host, ::path, ::query, ::fragment) + return "URL(" + listOf(::scheme, ::subScheme, ::userInfo, ::host, ::path, ::query, ::fragment) .map { it.name to it.get() } .filter { it.second != null } .joinToString(", ") { "${it.first}=${it.second}" } + ")" @@ -77,18 +79,55 @@ data class URL private constructor( else -> -1 } - operator fun invoke( - scheme: String?, - userInfo: String?, - host: String?, - path: String, - query: String?, - fragment: String?, - opaque: Boolean = false, - port: Int = DEFAULT_PORT - ): URL = URL(opaque, scheme?.lowercase(), userInfo, host, path, query, fragment, port) + fun fromComponents( + scheme: String? = null, + subScheme: String? = null, + userInfo: String? = null, + host: String? = null, + path: String = "", + query: String? = null, + fragment: String? = null, + opaque: Boolean = false, + port: Int = DEFAULT_PORT + ): URL = URL( + isOpaque = opaque, + scheme = scheme?.lowercase(), + subScheme = subScheme?.lowercase(), + userInfo = userInfo, + host = host, + path = path, + query = query, + fragment = fragment, + defaultPort = port + ) - private val schemeRegex = Regex("\\w+:") + @Deprecated( + message = "Use URL.fromComponents", + replaceWith = ReplaceWith("URL.fromComponents(scheme, subScheme, userInfo, host, path, query, fragment, opaque)") + ) + operator fun invoke( + scheme: String?, + userInfo: String?, + host: String?, + path: String, + query: String?, + fragment: String?, + opaque: Boolean = false, + port: Int = DEFAULT_PORT, + subScheme: String? = null, + ): URL = this.fromComponents( + opaque = opaque, + scheme = scheme?.lowercase(), + subScheme = subScheme?.lowercase(), + userInfo = userInfo, + host = host, + path = path, + query = query, + fragment = fragment, + port = port + ) + + private val schemeRegex = Regex("^([a-zA-Z0-9+.-]+)(?::([a-zA-Z]+))?:") operator fun invoke(url: String): URL { val r = StrReader(url) @@ -97,7 +136,9 @@ data class URL private constructor( schemeColon != null -> { val isHierarchical = r.tryLit("//") != null val nonScheme = r.readRemaining() - val scheme = schemeColon.dropLast(1) + val schemeParts = schemeColon.dropLast(1).split(":") + val scheme = schemeParts[0] + val subScheme = schemeParts.getOrNull(1) val nonFragment = nonScheme.substringBefore('#') val fragment = nonScheme.substringAfterOrNull('#') @@ -114,9 +155,10 @@ data class URL private constructor( val host = hostWithPort.substringBefore(':') val port = hostWithPort.substringAfterOrNull(':') - URL( + this.fromComponents( opaque = !isHierarchical, scheme = scheme, + subScheme = subScheme, userInfo = userInfo, host = host.takeIf { it.isNotEmpty() }, path = if (path != null) "/$path" else "", @@ -130,9 +172,10 @@ data class URL private constructor( val fragment = url.substringAfterOrNull('#') val path = nonFragment.substringBefore('?') val query = nonFragment.substringAfterOrNull('?') - URL( + this.fromComponents( opaque = false, scheme = null, + subScheme = null, userInfo = null, host = null, path = path, diff --git a/korge-core/test/korlibs/io/net/URLTest.kt b/korge-core/test/korlibs/io/net/URLTest.kt index b874768be..7fa6aa245 100644 --- a/korge-core/test/korlibs/io/net/URLTest.kt +++ b/korge-core/test/korlibs/io/net/URLTest.kt @@ -111,10 +111,66 @@ class URLTest { assertEquals("q=1", url.query) assertEquals("https://Google.com:442/test?q=1", url.fullUrl) - val url2 = URL(scheme = "HTTPs", userInfo = null, host = "Google.com", path = "", query = null, fragment = null) + val url2 = URL.fromComponents(scheme = "HTTPs", host = "Google.com") assertEquals("https", url2.scheme) assertEquals(443, url2.port) + assertNull(url2.subScheme) assertEquals("https://Google.com", url2.fullUrl) + + val url3 = URL("https://username:password@example.com:8443/path/to/resource?q=1") + assertEquals("https", url3.scheme) + assertNull(url3.subScheme) + assertEquals("username:password", url3.userInfo) + assertEquals("username", url3.user) + assertEquals("password", url3.password) + assertEquals("example.com", url3.host) + assertEquals(8443, url3.port) + assertEquals("/path/to/resource", url3.path) + assertEquals("/path/to/resource?q=1", url3.pathWithQuery) + assertEquals("q=1", url3.query) + assertEquals("https://username:password@example.com:8443/path/to/resource?q=1", url3.fullUrl) + } + + @Test + fun testSubSchemeUrls(){ +// :://:/?# + val url = URL("jdbc:mysql://localhost:3306/database?useSSL=false") + assertEquals("jdbc", url.scheme) // always lowercase issue #2092 + assertEquals("mysql", url.subScheme) + assertEquals("localhost", url.host) + assertEquals(3306, url.port) + assertEquals("/database", url.path) + assertEquals("/database?useSSL=false", url.pathWithQuery) + assertEquals("useSSL=false", url.query) + assertEquals("jdbc:mysql://localhost:3306/database?useSSL=false", url.fullUrl) + + val url2 = URL.fromComponents(scheme = "jdbc", subScheme = "mysql", host = "localhost", path = "/database", port = 3306) + assertEquals("jdbc:mysql://localhost:3306/database", url2.fullUrl) + + + val url3 = URL("jdbc:mysql://localhost:3306/database?useSSL=false") + assertEquals("jdbc", url3.scheme) // always lowercase issue #2092 + assertEquals("mysql", url3.subScheme) + assertEquals("localhost", url3.host) + assertEquals(3306, url3.port) + assertEquals("/database", url3.path) + assertEquals("/database?useSSL=false", url3.pathWithQuery) + assertEquals("useSSL=false", url3.query) + assertEquals("jdbc:mysql://localhost:3306/database?useSSL=false", url3.fullUrl) + } + + @Test + fun testUrlScheme() { +// test scheme standard (rfc3986#section-3.1): ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + val url = URL("custom+scheme123://example.com/path/to/resource") + assertEquals("custom+scheme123", url.scheme) + assertEquals("example.com", url.host) + assertEquals("custom+scheme123://example.com/path/to/resource", url.fullUrl) + + val url2 = URL("alpha-numeric+scheme.123://example.com/path/to/resource") + assertEquals("alpha-numeric+scheme.123", url2.scheme) + assertEquals("example.com", url2.host) + assertEquals("alpha-numeric+scheme.123://example.com/path/to/resource", url2.fullUrl) } @Test