diff --git a/app/src/main/java/chat/vojo/proxy/core/ConfigImport.kt b/app/src/main/java/chat/vojo/proxy/core/ConfigImport.kt index b67d441..7e3898c 100644 --- a/app/src/main/java/chat/vojo/proxy/core/ConfigImport.kt +++ b/app/src/main/java/chat/vojo/proxy/core/ConfigImport.kt @@ -55,6 +55,10 @@ private fun splitHostPort(hostPort: String): Pair? { val port = hp.substringAfterLast(':').toIntOrNull() ?: return null host to port } else { + // A bare (unbracketed) IPv6 literal has multiple colons and can't carry a port + // unambiguously — RFC 3986/5952 require brackets for IPv6+port. Reject rather than + // mis-split "fe80::1:8388" into a wrong host/port. + if (hp.count { it == ':' } > 1) return null val host = hp.substringBeforeLast(':') val port = hp.substringAfterLast(':').toIntOrNull() ?: return null host to port @@ -62,7 +66,12 @@ private fun splitHostPort(hostPort: String): Pair? { } private fun parseSs(s: String): ProxyConfig? { - val name = s.substringAfter('#', "").let { runCatching { URLDecoder.decode(it, "UTF-8") }.getOrDefault(it) } + // Percent-decode the #fragment name. URLDecoder does form-decoding, so it would turn a + // literal '+' into a space (mangling names like "C++ box"); escape '+' first so only real + // %XX escapes are decoded — matching java.net.URI.getFragment() used by parseUri. + val name = s.substringAfter('#', "").let { + runCatching { URLDecoder.decode(it.replace("+", "%2B"), "UTF-8") }.getOrDefault(it) + } // SIP002 transport plugins (v2ray-plugin, obfs, …) aren't supported — a // config that needs one won't tunnel, so reject it rather than import a // silently-broken server. @@ -114,7 +123,9 @@ private fun parseUri(s: String, type: ProxyType, defaultPort: Int): ProxyConfig? val port = if (uri.port > 0) uri.port else defaultPort val user = uri.userInfo?.substringBefore(':').orEmpty() val pass = uri.userInfo?.substringAfter(':', "").orEmpty() - val name = uri.fragment?.let { runCatching { URLDecoder.decode(it, "UTF-8") }.getOrDefault(it) }.orEmpty() + // URI.getFragment() is already percent-decoded; decoding again would turn '+' + // into a space and mangle names like "C++ box". + val name = uri.fragment.orEmpty() return ProxyConfig( name = name.ifBlank { host }, type = type,