1 /* <lambda>null2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package android.content.res.loader.cts 18 19 import android.content.Context 20 import android.content.res.AssetFileDescriptor 21 import android.content.res.Configuration 22 import android.content.res.Resources 23 import android.content.res.loader.AssetsProvider 24 import android.content.res.loader.ResourcesProvider 25 import android.os.ParcelFileDescriptor 26 import android.system.Os 27 import android.util.ArrayMap 28 import androidx.test.InstrumentationRegistry 29 import org.json.JSONObject 30 import org.junit.After 31 import org.junit.Assert.assertNull 32 import org.junit.Before 33 import java.io.Closeable 34 import java.io.FileOutputStream 35 import java.io.File 36 import java.io.FileDescriptor 37 import java.util.zip.ZipInputStream 38 39 abstract class ResourcesLoaderTestBase { 40 protected val PROVIDER_ONE: String = "CtsResourcesLoaderTests_ProviderOne" 41 protected val PROVIDER_TWO: String = "CtsResourcesLoaderTests_ProviderTwo" 42 protected val PROVIDER_THREE: String = "CtsResourcesLoaderTests_ProviderThree" 43 protected val PROVIDER_FOUR: String = "CtsResourcesLoaderTests_ProviderFour" 44 protected val PROVIDER_EMPTY: String = "empty" 45 46 companion object { 47 /** Converts the map to a stable JSON string representation. */ 48 fun mapToString(m: Map<String, String>): String { 49 return JSONObject(ArrayMap<Any?, Any?>().apply { putAll(m) }).toString() 50 } 51 52 /** Creates a lambda that runs multiple resources queries and concatenates the results. */ 53 fun query(queries: Map<String, (Resources) -> String>): Resources.() -> String { 54 return { 55 val resultMap = ArrayMap<String, String>() 56 queries.forEach { q -> 57 resultMap[q.key] = try { 58 q.value.invoke(this) 59 } catch (e: Exception) { 60 e.javaClass.simpleName 61 } 62 } 63 mapToString(resultMap) 64 } 65 } 66 } 67 68 // Data type of the current test iteration 69 open lateinit var dataType: DataType 70 71 protected lateinit var context: Context 72 protected lateinit var resources: Resources 73 74 // Track opened streams and ResourcesProviders to close them after testing 75 private val openedObjects = mutableListOf<Closeable>() 76 77 @Before 78 fun setUpBase() { 79 context = InstrumentationRegistry.getTargetContext() 80 .createConfigurationContext(Configuration()) 81 resources = context.resources 82 } 83 84 @After 85 fun removeAllLoaders() { 86 resources.clearLoaders() 87 context.applicationContext.resources.clearLoaders() 88 openedObjects.forEach { 89 try { 90 it.close() 91 } catch (ignored: Exception) { 92 } 93 } 94 } 95 96 protected fun String.openProvider( 97 dataType: DataType, 98 assetsProvider: MemoryAssetsProvider? = null 99 ): ResourcesProvider { 100 if (assetsProvider != null) { 101 openedObjects += assetsProvider 102 } 103 return when (dataType) { 104 DataType.APK_DISK_FD_NO_ASSETS_PROVIDER -> { 105 assertNull(assetsProvider) 106 val file = context.copiedAssetFile("$this.apk") 107 ResourcesProvider.loadFromApk(ParcelFileDescriptor.fromFd(file.fd)).also { 108 file.close() 109 } 110 } 111 DataType.APK_DISK_FD -> { 112 val file = context.copiedAssetFile("$this.apk") 113 ResourcesProvider.loadFromApk(ParcelFileDescriptor.fromFd(file.fd), 114 assetsProvider).also { 115 file.close() 116 } 117 } 118 DataType.APK_DISK_FD_OFFSETS -> { 119 val asset = context.assets.openFd("$this.apk") 120 ResourcesProvider.loadFromApk(asset.parcelFileDescriptor, asset.startOffset, 121 asset.length, assetsProvider).also { 122 asset.close() 123 } 124 } 125 DataType.ARSC_DISK_FD -> { 126 val file = context.copiedAssetFile("$this.arsc") 127 ResourcesProvider.loadFromTable(ParcelFileDescriptor.fromFd(file.fd), 128 assetsProvider).also { 129 file.close() 130 } 131 } 132 DataType.ARSC_DISK_FD_OFFSETS -> { 133 val asset = context.assets.openFd("$this.arsc") 134 ResourcesProvider.loadFromTable(asset.parcelFileDescriptor, asset.startOffset, 135 asset.length, assetsProvider).also { 136 asset.close() 137 } 138 } 139 DataType.APK_RAM_OFFSETS -> { 140 val asset = context.assets.openFd("$this.apk") 141 val leadingGarbageSize = 100L 142 val trailingGarbageSize = 55L 143 val fd = loadAssetIntoMemory(asset, leadingGarbageSize.toInt(), 144 trailingGarbageSize.toInt()) 145 ResourcesProvider.loadFromApk(fd, leadingGarbageSize, asset.declaredLength, 146 assetsProvider).also { 147 asset.close() 148 fd.close() 149 } 150 } 151 DataType.APK_RAM_FD -> { 152 val asset = context.assets.openFd("$this.apk") 153 var fd = loadAssetIntoMemory(asset) 154 ResourcesProvider.loadFromApk(fd, assetsProvider).also { 155 asset.close() 156 fd.close() 157 } 158 } 159 DataType.ARSC_RAM_MEMORY -> { 160 val asset = context.assets.openFd("$this.arsc") 161 var fd = loadAssetIntoMemory(asset) 162 ResourcesProvider.loadFromTable(fd, assetsProvider).also { 163 asset.close() 164 fd.close() 165 } 166 } 167 DataType.ARSC_RAM_MEMORY_OFFSETS -> { 168 val asset = context.assets.openFd("$this.arsc") 169 val leadingGarbageSize = 100L 170 val trailingGarbageSize = 55L 171 val fd = loadAssetIntoMemory(asset, leadingGarbageSize.toInt(), 172 trailingGarbageSize.toInt()) 173 ResourcesProvider.loadFromTable(fd, leadingGarbageSize, asset.declaredLength, 174 assetsProvider).also { 175 asset.close() 176 fd.close() 177 } 178 } 179 DataType.EMPTY -> { 180 if (equals(PROVIDER_EMPTY)) { 181 ResourcesProvider.empty(EmptyAssetsProvider()) 182 } else { 183 if (assetsProvider == null) ResourcesProvider.empty(ZipAssetsProvider(this)) 184 else ResourcesProvider.empty(assetsProvider) 185 } 186 } 187 DataType.DIRECTORY -> { 188 ResourcesProvider.loadFromDirectory(zipToDir("$this.apk").absolutePath, 189 assetsProvider) 190 } 191 DataType.SPLIT -> { 192 ResourcesProvider.loadFromSplit(context, "${this}_Split") 193 } 194 } 195 } 196 197 class EmptyAssetsProvider : AssetsProvider 198 199 /** An AssetsProvider that reads from a zip asset. */ 200 inner class ZipAssetsProvider(val providerName: String) : AssetsProvider { 201 val root: File = zipToDir("$providerName.apk") 202 203 override fun loadAssetFd(path: String, accessMode: Int): AssetFileDescriptor? { 204 val f = File(root, path) 205 return if (f.exists()) AssetFileDescriptor( 206 ParcelFileDescriptor.open(File(root, path), 207 ParcelFileDescriptor.MODE_READ_ONLY), 0, 208 AssetFileDescriptor.UNKNOWN_LENGTH) else null 209 } 210 } 211 212 /** AssetsProvider for testing that returns file descriptors to files in RAM. */ 213 class MemoryAssetsProvider : AssetsProvider, Closeable { 214 var loadAssetResults = HashMap<String, FileDescriptor>() 215 216 fun addLoadAssetFdResult(path: String, value: String) = apply { 217 val fd = Os.memfd_create(path, 0) 218 val valueBytes = value.toByteArray() 219 Os.write(fd, valueBytes, 0, valueBytes.size) 220 loadAssetResults[path] = fd 221 } 222 223 override fun loadAssetFd(path: String, accessMode: Int): AssetFileDescriptor? { 224 return if (loadAssetResults.containsKey(path)) AssetFileDescriptor( 225 ParcelFileDescriptor.dup(loadAssetResults[path]), 0, 226 AssetFileDescriptor.UNKNOWN_LENGTH) else null 227 } 228 229 override fun close() { 230 for (f in loadAssetResults.values) { 231 Os.close(f) 232 } 233 } 234 } 235 236 /** Extracts an archive-based asset into a directory on disk. */ 237 private fun zipToDir(name: String): File { 238 val root = File(context.filesDir, name.split('.')[0]) 239 if (root.exists()) { 240 return root 241 } 242 243 root.mkdir() 244 ZipInputStream(context.assets.open(name)).use { zis -> 245 while (true) { 246 val entry = zis.nextEntry ?: break 247 val file = File(root, entry.name) 248 if (entry.isDirectory) { 249 continue 250 } 251 252 file.parentFile.mkdirs() 253 file.outputStream().use { output -> 254 var b = zis.read() 255 while (b != -1) { 256 output.write(b) 257 b = zis.read() 258 } 259 } 260 } 261 } 262 return root 263 } 264 265 /** Loads the asset into a temporary file stored in RAM. */ 266 private fun loadAssetIntoMemory( 267 asset: AssetFileDescriptor, 268 leadingGarbageSize: Int = 0, 269 trailingGarbageSize: Int = 0 270 ): ParcelFileDescriptor { 271 val originalFd = Os.memfd_create(asset.toString(), 0 /* flags */) 272 val fd = ParcelFileDescriptor.dup(originalFd) 273 Os.close(originalFd) 274 275 val input = asset.createInputStream() 276 FileOutputStream(fd.fileDescriptor).use { output -> 277 // Add garbage before the APK data 278 for (i in 0 until leadingGarbageSize) { 279 output.write(Math.random().toInt()) 280 } 281 282 for (i in 0 until asset.length.toInt()) { 283 output.write(input.read()) 284 } 285 286 // Add garbage after the APK data 287 for (i in 0 until trailingGarbageSize) { 288 output.write(Math.random().toInt()) 289 } 290 } 291 292 return fd 293 } 294 295 enum class DataType { 296 APK_DISK_FD_NO_ASSETS_PROVIDER, 297 APK_DISK_FD, 298 APK_DISK_FD_OFFSETS, 299 APK_RAM_FD, 300 APK_RAM_OFFSETS, 301 ARSC_DISK_FD, 302 ARSC_DISK_FD_OFFSETS, 303 ARSC_RAM_MEMORY, 304 ARSC_RAM_MEMORY_OFFSETS, 305 EMPTY, 306 DIRECTORY, 307 SPLIT 308 } 309 } 310