diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt index f541cdb3..5f9f64a5 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt @@ -22,17 +22,17 @@ open class FmtBase { val query = if (dicQuery != null) "?" + dicQuery.toList().joinToString( separator = "&", - transform = { it.first + "=" + Utils.urlEncode(it.second) }) + transform = { it.first + "=" + Utils.encodeURIComponent(it.second) }) else "" val url = String.format( "%s@%s:%s", - Utils.urlEncode(userInfo ?: ""), + Utils.encodeURIComponent(userInfo ?: ""), Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())), config.serverPort ) - return "${url}${query}#${Utils.urlEncode(config.remarks)}" + return "${url}${query}#${Utils.encodeURIComponent(config.remarks)}" } /** @@ -43,7 +43,7 @@ open class FmtBase { */ fun getQueryParam(uri: URI): Map { return uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + .associate { it.split("=").let { (k, v) -> k to Utils.decodeURIComponent(v) } } } /** diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt index b136e3b0..cfc127b2 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt @@ -26,7 +26,7 @@ object Hysteria2Fmt : FmtBase() { val config = ProfileItem.create(EConfigType.HYSTERIA2) val uri = URI(Utils.fixIllegalUrl(str)) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } + config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.server = uri.idnHost config.serverPort = uri.port.toString() config.password = uri.userInfo diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt index ce644c75..94d0aaed 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt @@ -36,7 +36,7 @@ object ShadowsocksFmt : FmtBase() { if (uri.port <= 0) return null if (uri.userInfo.isNullOrEmpty()) return null - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } + config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.server = uri.idnHost config.serverPort = uri.port.toString() @@ -83,7 +83,7 @@ object ShadowsocksFmt : FmtBase() { if (indexSplit > 0) { try { config.remarks = - Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + Utils.decodeURIComponent(result.substring(indexSplit + 1, result.length)) } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt index f629b9f3..0ba2613d 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt @@ -23,7 +23,7 @@ object SocksFmt : FmtBase() { if (uri.idnHost.isEmpty()) return null if (uri.port <= 0) return null - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } + config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.server = uri.idnHost config.serverPort = uri.port.toString() diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt index 70280c57..022ab640 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt @@ -23,7 +23,7 @@ object TrojanFmt : FmtBase() { val config = ProfileItem.create(EConfigType.TROJAN) val uri = URI(Utils.fixIllegalUrl(str)) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } + config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.server = uri.idnHost config.serverPort = uri.port.toString() config.password = uri.userInfo diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt index be5e90c4..8f6d05a8 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt @@ -26,7 +26,7 @@ object VlessFmt : FmtBase() { if (uri.rawQuery.isNullOrEmpty()) return null val queryParam = getQueryParam(uri) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } + config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.server = uri.idnHost config.serverPort = uri.port.toString() config.password = uri.userInfo diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt index 01ce09fb..c97d7102 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt @@ -163,7 +163,7 @@ object VmessFmt : FmtBase() { if (uri.rawQuery.isNullOrEmpty()) return null val queryParam = getQueryParam(uri) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } + config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.server = uri.idnHost config.serverPort = uri.port.toString() config.password = uri.userInfo diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt index bc07b114..006aa2d8 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt @@ -26,7 +26,7 @@ object WireguardFmt : FmtBase() { if (uri.rawQuery.isNullOrEmpty()) return null val queryParam = getQueryParam(uri) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } + config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.server = uri.idnHost config.serverPort = uri.port.toString() diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt index 8b9281c6..d07c2469 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt @@ -328,13 +328,48 @@ object Utils { */ fun urlEncode(url: String): String { return try { - URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20") + URLEncoder.encode(url, Charsets.UTF_8.toString()) } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to encode URL", e) url } } + /** + * Decode a "encodeURIComponent" string. + * + * @param url The "encodeURIComponent" string. + * @return The decoded string, or the original string if decoding fails. + */ + fun decodeURIComponent(url: String): String { + return try { + // Decode strictly according to RFC 3986 / encodeURIComponent semantics. + // '+' is a literal plus and MUST NOT be interpreted as space. + // Inputs using '+' for spaces are non-conforming and rejected deliberately + // to avoid cross-language interoperability issues. + URLDecoder.decode(url.replace("+", "%2B"), Charsets.UTF_8.toString()) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to decode encodeURIComponent", e) + url + } + } + + /** + * Encode a string to "encodeURIComponent" format. + * + * @param url The string to encode. + * @return The "encodeURIComponent" encoded string, or the original string if encoding fails. + */ + fun encodeURIComponent(url: String): String { + return try { + // Replace '+' with '%20' to conform to encodeURIComponent semantics. + URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20") + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to encode encodeURIComponent", e) + url + } + } + /** * Read text from an asset file. *