Skip to content

Commit 4e9f70a

Browse files
yanglwshifujun
authored andcommitted
fix(core.loader): 修复容器authority和插件authority相同时返回错误 Uri 的问题
1 parent aff2f05 commit 4e9f70a

3 files changed

Lines changed: 160 additions & 1 deletion

File tree

projects/sdk/core/loader/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ dependencies {
3939
compileOnly project(':common')
4040
api project(':load-parameters')
4141
compileOnly files(AndroidJar.ANDROID_JAR_PATH)
42+
testImplementation "org.mockito:mockito-inline:5.2.0"
43+
testImplementation files(AndroidJar.ANDROID_JAR_PATH)
4244
}
4345

4446
def generateCode = tasks.register('generateCode') {

projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/managers/PluginContentProviderManager.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,42 @@ class PluginContentProviderManager() : UriConverter.UriParseDelegate {
138138

139139
fun convert2PluginUri(uri: Uri): Uri {
140140
val containerAuthority: String? = uri.authority
141-
if (!providerAuthorityMap.values.contains(containerAuthority)) {
141+
val matchAuthorityMap = providerAuthorityMap.filter { it.value == containerAuthority }
142+
if (matchAuthorityMap.isEmpty()) {
142143
throw IllegalArgumentException("不能识别的uri Authority:$containerAuthority")
143144
}
144145
val uriString = uri.toString()
146+
for (entry in matchAuthorityMap) {
147+
val pluginAuthority = entry.key
148+
// 通过正则表达式去除 containerAuthority ,支持以下场景:
149+
// 1. content://containerAuthority/pluginAuthority(插件内部调用 insert 、query 等方法)
150+
// 2. content://containerAuthority/containerAuthority/pluginAuthority(插件内部调用 call 方法)
151+
// 3. content://containerAuthority (外部应用调用 content provider 方法且 containerAuthority == pluginAuthority)
152+
// 正则表达式结构分解:
153+
// - ^content://:
154+
// - 作用:强制从字符串的最开始进行匹配。
155+
// - 目的:确保只处理标准的 content 协议 URI。
156+
// - ((?:$escapedContainer/)*)(捕获组 1):
157+
// - $escapedContainer/:这是经过 Regex.escape() 处理后的容器 Authority 字符串,后面紧跟一个斜杠。转义确保了如 a.b 中的点号不会匹配任意字符。
158+
// - (?:...):非捕获组,仅用于将“容器名+斜杠”作为一个整体进行多次匹配。
159+
// - *(贪婪匹配):匹配零个或多个连续的容器前缀。使用贪婪模式是为了在 containerAuthority 和 pluginAuthority 相同的情况下(如content://A/A/path),尽可能多地剥离外层容器,只留下最后一个作为插件标识。
160+
// - $escapedPlugin:
161+
// - 作用:匹配插件真实的 Authority 。它是整个正则的锚点,用于确定这个 URI 属于哪个插件。
162+
// - (?=/|$)(正向肯定预查):
163+
// - 作用:这是一个非占位匹配,要求匹配到的 pluginAuthority 后面必须紧跟一个斜杠 /(表示路径开始)或者字符串结束符 $ 。
164+
// - 目的:防止部分匹配。例如,如果 pluginAuthority 是 A,而 URI 是 content://Ab/path,如果没有这个预查,正则会错误地匹配到 Ab 。
165+
val escapedContainer = Regex.escape(containerAuthority!!)
166+
val escapedPlugin = Regex.escape(pluginAuthority)
167+
val regex = Regex("^$CONTENT_PREFIX((?:$escapedContainer/)*)$escapedPlugin(?=/|$)")
168+
169+
// 可能存在一个 containerAuthority 匹配多个 pluginAuthority 的场景,所以存在无法匹配的场景
170+
val matchResult = regex.find(uriString) ?: continue
171+
// 如果找到了匹配的内容,则剔除匹配的 containerAuthority 内容
172+
val range = matchResult.groups[1]!!.range
173+
return Uri.parse(
174+
uriString.substring(0, range.first) + uriString.substring(range.last + 1)
175+
)
176+
}
145177
return Uri.parse(uriString.replace("$containerAuthority/", ""))
146178
}
147179

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Tencent is pleased to support the open source community by making Tencent Shadow available.
3+
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
4+
*
5+
* Licensed under the BSD 3-Clause License (the "License"); you may not use
6+
* this file except in compliance with the License. You may obtain a copy of
7+
* the License at
8+
*
9+
* https://opensource.org/licenses/BSD-3-Clause
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package com.tencent.shadow.core.loader.managers
20+
21+
import android.net.Uri
22+
import com.tencent.shadow.core.loader.infos.ContainerProviderInfo
23+
import com.tencent.shadow.core.runtime.PluginManifest
24+
import org.junit.Assert
25+
import org.junit.Before
26+
import org.junit.Test
27+
import org.junit.experimental.runners.Enclosed
28+
import org.junit.runner.RunWith
29+
import org.junit.runners.Parameterized
30+
import org.junit.runners.Parameterized.Parameters
31+
import org.mockito.ArgumentMatchers.anyString
32+
import org.mockito.Mockito.mock
33+
import org.mockito.Mockito.mockStatic
34+
import org.mockito.Mockito.`when`
35+
36+
@RunWith(Enclosed::class)
37+
class PluginContentProviderManagerTest {
38+
39+
@RunWith(Parameterized::class)
40+
class Convert2PluginUriTest(
41+
private val containerAuthority: String,
42+
private val pluginAuthority: String,
43+
private val input: String,
44+
private val expected: String
45+
) {
46+
private lateinit var manager: PluginContentProviderManager
47+
48+
companion object {
49+
@JvmStatic
50+
@Parameters
51+
fun data(): Collection<Array<String>> = listOf(
52+
"com.container.auth" to "com.plugin.auth",
53+
"com.container.auth" to "com.container.auth"
54+
)
55+
.flatMap { (containerAuthority, pluginAuthority) ->
56+
val same = containerAuthority == pluginAuthority
57+
listOf(
58+
"content://$containerAuthority" to "content://$containerAuthority",
59+
"content://$containerAuthority/" to if (same) "content://$pluginAuthority/" else "content://",
60+
"content://$containerAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://path",
61+
"content://$containerAuthority/$pluginAuthority" to "content://$pluginAuthority",
62+
"content://$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/",
63+
"content://$containerAuthority/$pluginAuthority/path" to "content://$pluginAuthority/path",
64+
"content://$containerAuthority/$containerAuthority/$pluginAuthority" to "content://$pluginAuthority",
65+
"content://$containerAuthority/$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/",
66+
"content://$containerAuthority/$containerAuthority/$pluginAuthority/path" to "content://$pluginAuthority/path",
67+
"content://$containerAuthority/$pluginAuthority/$containerAuthority" to if (same) "content://$pluginAuthority" else "content://$pluginAuthority/$containerAuthority",
68+
"content://$containerAuthority/$pluginAuthority/$containerAuthority/" to if (same) "content://$pluginAuthority/" else "content://$pluginAuthority/$containerAuthority/",
69+
"content://$containerAuthority/$pluginAuthority/$containerAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://$pluginAuthority/$containerAuthority/path",
70+
"content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority" to if (same) "content://$pluginAuthority" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority",
71+
"content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority/" to if (same) "content://$pluginAuthority/" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority/",
72+
"content://$containerAuthority/$pluginAuthority/$containerAuthority/$pluginAuthority/path" to if (same) "content://$pluginAuthority/path" else "content://$pluginAuthority/$containerAuthority/$pluginAuthority/path",
73+
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority" to "content://$pluginAuthority/path/$containerAuthority",
74+
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/" to "content://$pluginAuthority/path/$containerAuthority/",
75+
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/file" to "content://$pluginAuthority/path/$containerAuthority/file",
76+
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority" to "content://$pluginAuthority/path/$pluginAuthority",
77+
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/" to "content://$pluginAuthority/path/$pluginAuthority/",
78+
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/file" to "content://$pluginAuthority/path/$pluginAuthority/file",
79+
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority",
80+
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority/" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority/",
81+
"content://$containerAuthority/$pluginAuthority/path/$containerAuthority/$pluginAuthority/file" to "content://$pluginAuthority/path/$containerAuthority/$pluginAuthority/file",
82+
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority",
83+
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority/" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority/",
84+
"content://$containerAuthority/$pluginAuthority/path/$pluginAuthority/$containerAuthority/file" to "content://$pluginAuthority/path/$pluginAuthority/$containerAuthority/file"
85+
)
86+
.map { arrayOf(containerAuthority, pluginAuthority, it.first, it.second) }
87+
}
88+
}
89+
90+
@Before
91+
fun init() {
92+
manager = PluginContentProviderManager().apply {
93+
addContentProviderInfo(
94+
"partKey",
95+
PluginManifest.ProviderInfo("pluginClassName", pluginAuthority, true),
96+
ContainerProviderInfo("containerClassName", containerAuthority),
97+
pluginAuthority
98+
)
99+
}
100+
}
101+
102+
@Test
103+
fun testConvert2PluginUri() {
104+
mockStatic(Uri::class.java).use {
105+
it.`when`<Uri> { Uri.parse(anyString()) }
106+
.thenAnswer { invocation -> mockUri(invocation.getArgument(0)) }
107+
108+
Assert.assertEquals(expected, manager.convert2PluginUri(mockUri(input)).toString())
109+
}
110+
}
111+
}
112+
}
113+
114+
private fun mockUri(input: String): Uri {
115+
val uri = mock(Uri::class.java)
116+
`when`(uri.toString()).thenReturn(input)
117+
118+
val startIndex = "content://".length
119+
val indexOf = input.indexOf('/', startIndex)
120+
val endIndex = if (indexOf < 0) input.length else indexOf
121+
val authority = input.substring(startIndex, endIndex)
122+
`when`(uri.authority).thenReturn(authority)
123+
124+
return uri
125+
}

0 commit comments

Comments
 (0)