From 2acc11cc7b11e0f8d759b31fd3e03b2a29dbf85c Mon Sep 17 00:00:00 2001 From: yanglw Date: Mon, 23 Mar 2026 22:50:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(core.loader):=20=E4=BF=AE=E5=A4=8D=E5=AE=B9?= =?UTF-8?q?=E5=99=A8authority=E5=92=8C=E6=8F=92=E4=BB=B6authority=E7=9B=B8?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E8=BF=94=E5=9B=9E=E9=94=99=E8=AF=AF=20Uri=20?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- projects/sdk/core/loader/build.gradle | 2 + .../managers/PluginContentProviderManager.kt | 34 ++++- .../PluginContentProviderManagerTest.kt | 125 ++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 projects/sdk/core/loader/src/test/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManagerTest.kt diff --git a/projects/sdk/core/loader/build.gradle b/projects/sdk/core/loader/build.gradle index a0a49cb21..02f810bb3 100644 --- a/projects/sdk/core/loader/build.gradle +++ b/projects/sdk/core/loader/build.gradle @@ -39,6 +39,8 @@ dependencies { compileOnly project(':common') api project(':load-parameters') compileOnly files(AndroidJar.ANDROID_JAR_PATH) + testImplementation "org.mockito:mockito-inline:5.2.0" + testImplementation files(AndroidJar.ANDROID_JAR_PATH) } def generateCode = tasks.register('generateCode') { diff --git a/projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManager.kt b/projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManager.kt index 135c77d72..d1db876b4 100644 --- a/projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManager.kt +++ b/projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManager.kt @@ -138,10 +138,42 @@ class PluginContentProviderManager() : UriConverter.UriParseDelegate { fun convert2PluginUri(uri: Uri): Uri { val containerAuthority: String? = uri.authority - if (!providerAuthorityMap.values.contains(containerAuthority)) { + val matchAuthorityMap = providerAuthorityMap.filter { it.value == containerAuthority } + if (matchAuthorityMap.isEmpty()) { throw IllegalArgumentException("不能识别的uri Authority:$containerAuthority") } val uriString = uri.toString() + for (entry in matchAuthorityMap) { + val pluginAuthority = entry.key + // 通过正则表达式去除 containerAuthority ,支持以下场景: + // 1. content://containerAuthority/pluginAuthority(插件内部调用 insert 、query 等方法) + // 2. content://containerAuthority/containerAuthority/pluginAuthority(插件内部调用 call 方法) + // 3. content://containerAuthority (外部应用调用 content provider 方法且 containerAuthority == pluginAuthority) + // 正则表达式结构分解: + // - ^content://: + // - 作用:强制从字符串的最开始进行匹配。 + // - 目的:确保只处理标准的 content 协议 URI。 + // - ((?:$escapedContainer/)*)(捕获组 1): + // - $escapedContainer/:这是经过 Regex.escape() 处理后的容器 Authority 字符串,后面紧跟一个斜杠。转义确保了如 a.b 中的点号不会匹配任意字符。 + // - (?:...):非捕获组,仅用于将“容器名+斜杠”作为一个整体进行多次匹配。 + // - *(贪婪匹配):匹配零个或多个连续的容器前缀。使用贪婪模式是为了在 containerAuthority 和 pluginAuthority 相同的情况下(如content://A/A/path),尽可能多地剥离外层容器,只留下最后一个作为插件标识。 + // - $escapedPlugin: + // - 作用:匹配插件真实的 Authority 。它是整个正则的锚点,用于确定这个 URI 属于哪个插件。 + // - (?=/|$)(正向肯定预查): + // - 作用:这是一个非占位匹配,要求匹配到的 pluginAuthority 后面必须紧跟一个斜杠 /(表示路径开始)或者字符串结束符 $ 。 + // - 目的:防止部分匹配。例如,如果 pluginAuthority 是 A,而 URI 是 content://Ab/path,如果没有这个预查,正则会错误地匹配到 Ab 。 + val escapedContainer = Regex.escape(containerAuthority!!) + val escapedPlugin = Regex.escape(pluginAuthority) + val regex = Regex("^$CONTENT_PREFIX((?:$escapedContainer/)*)$escapedPlugin(?=/|$)") + + // 可能存在一个 containerAuthority 匹配多个 pluginAuthority 的场景,所以存在无法匹配的场景 + val matchResult = regex.find(uriString) ?: continue + // 如果找到了匹配的内容,则剔除匹配的 containerAuthority 内容 + val range = matchResult.groups[1]!!.range + return Uri.parse( + uriString.substring(0, range.first) + uriString.substring(range.last + 1) + ) + } return Uri.parse(uriString.replace("$containerAuthority/", "")) } diff --git a/projects/sdk/core/loader/src/test/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManagerTest.kt b/projects/sdk/core/loader/src/test/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManagerTest.kt new file mode 100644 index 000000000..9f5aa6029 --- /dev/null +++ b/projects/sdk/core/loader/src/test/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManagerTest.kt @@ -0,0 +1,125 @@ +/* + * Tencent is pleased to support the open source community by making Tencent Shadow available. + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.tencent.shadow.core.loader.managers + +import android.net.Uri +import com.tencent.shadow.core.loader.infos.ContainerProviderInfo +import com.tencent.shadow.core.runtime.PluginManifest +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.experimental.runners.Enclosed +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock +import org.mockito.Mockito.mockStatic +import org.mockito.Mockito.`when` + +@RunWith(Enclosed::class) +class PluginContentProviderManagerTest { + + @RunWith(Parameterized::class) + class Convert2PluginUriTest( + private val containerAuthority: String, + private val pluginAuthority: String, + private val input: String, + private val expected: String + ) { + private lateinit var manager: PluginContentProviderManager + + companion object { + @JvmStatic + @Parameters + fun data(): Collection> = listOf( + "com.container.auth" to "com.plugin.auth", + "com.container.auth" to "com.container.auth" + ) + .flatMap { (containerAuthority, pluginAuthority) -> + val same = containerAuthority == pluginAuthority + listOf( + "content://$containerAuthority" to "content://$containerAuthority", + "content://$containerAuthority/" to if (same) "content://$pluginAuthority/" else "content://", + "content://$containerAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://path", + "content://$containerAuthority/$pluginAuthority" to "content://$pluginAuthority", + "content://$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/", + "content://$containerAuthority/$pluginAuthority/path" to "content://$pluginAuthority/path", + "content://$containerAuthority/$containerAuthority/$pluginAuthority" to "content://$pluginAuthority", + "content://$containerAuthority/$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/", + "content://$containerAuthority/$containerAuthority/$pluginAuthority/path" to "content://$pluginAuthority/path", + "content://$containerAuthority/$pluginAuthority/$containerAuthority" to if (same) "content://$pluginAuthority" else "content://$pluginAuthority/$containerAuthority", + "content://$containerAuthority/$pluginAuthority/$containerAuthority/" to if (same) "content://$pluginAuthority/" else "content://$pluginAuthority/$containerAuthority/", + "content://$containerAuthority/$pluginAuthority/$containerAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://$pluginAuthority/$containerAuthority/path", + "content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority" to if (same) "content://$pluginAuthority" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority", + "content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority/" to if (same) "content://$pluginAuthority/" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority/", + "content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority/path", + "content://$containerAuthority/$pluginAuthority/path/$containerAuthority" to "content://$pluginAuthority/path/$containerAuthority", + "content://$containerAuthority/$pluginAuthority/path/$containerAuthority/" to "content://$pluginAuthority/path/$containerAuthority/", + "content://$containerAuthority/$pluginAuthority/path/$containerAuthority/file" to "content://$pluginAuthority/path/$containerAuthority/file", + "content://$containerAuthority/$pluginAuthority/path/$pluginAuthority" to "content://$pluginAuthority/path/$pluginAuthority", + "content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/" to "content://$pluginAuthority/path/$pluginAuthority/", + "content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/file" to "content://$pluginAuthority/path/$pluginAuthority/file", + "content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority", + "content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority/", + "content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority/file" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority/file", + "content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority", + "content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority/" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority/", + "content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority/file" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority/file" + ) + .map { arrayOf(containerAuthority, pluginAuthority, it.first, it.second) } + } + } + + @Before + fun init() { + manager = PluginContentProviderManager().apply { + addContentProviderInfo( + "partKey", + PluginManifest.ProviderInfo("pluginClassName", pluginAuthority, true), + ContainerProviderInfo("containerClassName", containerAuthority), + pluginAuthority + ) + } + } + + @Test + fun testConvert2PluginUri() { + mockStatic(Uri::class.java).use { + it.`when` { Uri.parse(anyString()) } + .thenAnswer { invocation -> mockUri(invocation.getArgument(0)) } + + Assert.assertEquals(expected, manager.convert2PluginUri(mockUri(input)).toString()) + } + } + } +} + +private fun mockUri(input: String): Uri { + val uri = mock(Uri::class.java) + `when`(uri.toString()).thenReturn(input) + + val startIndex = "content://".length + val indexOf = input.indexOf('/', startIndex) + val endIndex = if (indexOf < 0) input.length else indexOf + val authority = input.substring(startIndex, endIndex) + `when`(uri.authority).thenReturn(authority) + + return uri +} \ No newline at end of file