fix(import): percent-decode share-link names preserving '+'; reject ambiguous bare IPv6

This commit is contained in:
heaven 2026-06-16 12:30:53 +03:00
parent fbeb15be88
commit 9b5a3dee1d

View file

@ -55,6 +55,10 @@ private fun splitHostPort(hostPort: String): Pair<String, Int>? {
val port = hp.substringAfterLast(':').toIntOrNull() ?: return null val port = hp.substringAfterLast(':').toIntOrNull() ?: return null
host to port host to port
} else { } 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 host = hp.substringBeforeLast(':')
val port = hp.substringAfterLast(':').toIntOrNull() ?: return null val port = hp.substringAfterLast(':').toIntOrNull() ?: return null
host to port host to port
@ -62,7 +66,12 @@ private fun splitHostPort(hostPort: String): Pair<String, Int>? {
} }
private fun parseSs(s: String): ProxyConfig? { 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 // SIP002 transport plugins (v2ray-plugin, obfs, …) aren't supported — a
// config that needs one won't tunnel, so reject it rather than import a // config that needs one won't tunnel, so reject it rather than import a
// silently-broken server. // 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 port = if (uri.port > 0) uri.port else defaultPort
val user = uri.userInfo?.substringBefore(':').orEmpty() val user = uri.userInfo?.substringBefore(':').orEmpty()
val pass = uri.userInfo?.substringAfter(':', "").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( return ProxyConfig(
name = name.ifBlank { host }, name = name.ifBlank { host },
type = type, type = type,