1 /*
2  * 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 com.android.server.pm.parsing
18 
19 import android.content.Context
20 import android.content.pm.ActivityInfo
21 import android.content.pm.ApplicationInfo
22 import android.content.pm.ComponentInfo
23 import android.content.pm.ConfigurationInfo
24 import android.content.pm.FeatureInfo
25 import android.content.pm.InstrumentationInfo
26 import android.content.pm.PackageInfo
27 import android.content.pm.PackageParser
28 import android.content.pm.PackageUserState
29 import android.content.pm.PermissionInfo
30 import android.content.pm.ProviderInfo
31 import android.content.pm.ServiceInfo
32 import android.os.Bundle
33 import android.os.Debug
34 import android.os.Environment
35 import android.os.Process
36 import android.util.SparseArray
37 import androidx.test.platform.app.InstrumentationRegistry
38 import com.android.server.pm.PackageManagerService
39 import com.android.server.pm.PackageSetting
40 import com.android.server.pm.parsing.pkg.AndroidPackage
41 import com.android.server.pm.pkg.PackageStateUnserialized
42 import com.android.server.testutils.mockThrowOnUnmocked
43 import com.android.server.testutils.whenever
44 import org.junit.BeforeClass
45 import org.mockito.Mockito.any
46 import org.mockito.Mockito.anyBoolean
47 import org.mockito.Mockito.anyInt
48 import org.mockito.Mockito.anyString
49 import org.mockito.Mockito.mock
50 import java.io.File
51 
52 open class AndroidPackageParsingTestBase {
53 
54     companion object {
55 
56         private const val VERIFY_ALL_APKS = true
57 
58         // For auditing memory usage differences to /sdcard/AndroidPackageParsingTestBase.hprof
59         private const val DUMP_HPROF_TO_EXTERNAL = false
60 
61         val context: Context = InstrumentationRegistry.getInstrumentation().getContext()
62         protected val packageParser = PackageParser().apply {
63             setOnlyCoreApps(false)
64             setDisplayMetrics(context.resources.displayMetrics)
65             setCallback { false /* hasFeature */ }
66         }
67 
68         protected val packageParser2 = PackageParser2.forParsingFileWithDefaults()
69 
70         /**
71          * It would be difficult to mock all possibilities, so just use the APKs on device.
72          * Unfortunately, this means the device must be bootable to verify potentially
73          * boot-breaking behavior.
74          */
75         private val apks = mutableListOf(File(Environment.getRootDirectory(), "framework"))
76                 .apply {
77                     @Suppress("ConstantConditionIf")
78                     if (VERIFY_ALL_APKS) {
79                         this += (PackageManagerService.SYSTEM_PARTITIONS)
80                                 .flatMap {
81                                     listOfNotNull(it.privAppFolder, it.appFolder, it.overlayFolder)
82                                 }
83                     }
84                 }
85                 .flatMap {
86                     it.walkTopDown()
87                             .filter { file -> file.name.endsWith(".apk") }
88                             .toList()
89                 }
90                 .distinct()
91 
92         private val dummyUserState = mock(PackageUserState::class.java).apply {
93             installed = true
94             whenever(isAvailable(anyInt())) { true }
95             whenever(isMatch(any<ComponentInfo>(), anyInt())) { true }
96             whenever(isMatch(anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
97                     anyString(), anyInt())) { true }
98         }
99 
100         val oldPackages = mutableListOf<PackageParser.Package>()
101 
102         val newPackages = mutableListOf<AndroidPackage>()
103 
104         @Suppress("ConstantConditionIf")
105         @JvmStatic
106         @BeforeClass
107         fun setUpPackages() {
108             var uid = Process.FIRST_APPLICATION_UID
109             apks.mapNotNull {
110                 try {
111                     packageParser.parsePackage(it, PackageParser.PARSE_IS_SYSTEM_DIR, false) to
112                             packageParser2.parsePackage(it, PackageParser.PARSE_IS_SYSTEM_DIR,
113                                     false)
114                 } catch (ignored: Exception) {
115                     // It is intentional that a failure of either call here will result in failing
116                     // both. Having null on one side would mean nothing to compare. Due to the
117                     // nature of presubmit, this may not be caused by the change being tested, so
118                     // it's unhelpful to consider it a failure. Actual parsing issues will be
119                     // reported by SystemPartitionParseTest in postsubmit.
120                     null
121                 }
122             }.forEach { (old, new) ->
123                 // Assign an arbitrary UID. This is normally done after parsing completes, inside
124                 // PackageManagerService, but since that code isn't run here, need to mock it. This
125                 // is equivalent to what the system would assign.
126                 old.applicationInfo.uid = uid
127                 new.uid = uid
128                 uid++
129 
130                 oldPackages += old
131                 newPackages += new.hideAsFinal()
132             }
133 
134             if (DUMP_HPROF_TO_EXTERNAL) {
135                 System.gc()
136                 Environment.getExternalStorageDirectory()
137                         .resolve(
138                                 "${AndroidPackageParsingTestBase::class.java.simpleName}.hprof")
139                         .absolutePath
140                         .run(Debug::dumpHprofData)
141             }
142         }
143 
144         fun oldAppInfo(
145             pkg: PackageParser.Package,
146             flags: Int = 0,
147             userId: Int = 0
148         ): ApplicationInfo? {
149             return PackageParser.generateApplicationInfo(pkg, flags, dummyUserState, userId)
150         }
151 
152         fun newAppInfo(
153             pkg: AndroidPackage,
154             flags: Int = 0,
155             userId: Int = 0
156         ): ApplicationInfo? {
157             return PackageInfoUtils.generateApplicationInfo(pkg, flags, dummyUserState, userId,
158                     mockPkgSetting(pkg))
159         }
160 
161         fun newAppInfoWithoutState(
162             pkg: AndroidPackage,
163             flags: Int = 0,
164             userId: Int = 0
165         ): ApplicationInfo? {
166             return PackageInfoUtils.generateApplicationInfo(pkg, flags, dummyUserState, userId,
167                     mockPkgSetting(pkg))
168         }
169 
170         fun oldPackageInfo(pkg: PackageParser.Package, flags: Int = 0): PackageInfo? {
171             return PackageParser.generatePackageInfo(pkg, intArrayOf(), flags, 5, 6, emptySet(),
172                     dummyUserState)
173         }
174 
175         fun newPackageInfo(pkg: AndroidPackage, flags: Int = 0): PackageInfo? {
176             return PackageInfoUtils.generate(pkg, intArrayOf(), flags, 5, 6, emptySet(),
177                     dummyUserState, 0, mockPkgSetting(pkg))
178         }
179 
180         private fun mockPkgSetting(aPkg: AndroidPackage) = mockThrowOnUnmocked<PackageSetting> {
181             this.pkg = aPkg
182             this.appId = aPkg.uid
183             whenever(pkgState) { PackageStateUnserialized() }
184             whenever(readUserState(anyInt())) { dummyUserState }
185         }
186     }
187 
188     // The following methods dump an exact set of fields from the object to compare, because
189     // 1. comprehensive equals/toStrings do not exist on all of the Info objects, and
190     // 2. the test must only verify fields that [PackageParser.Package] can actually fill, as
191     // no new functionality will be added to it.
192 
193     // The following methods prepend "this." because @hide APIs can cause an IDE to auto-import
194     // the R.attr constant instead of referencing the field in an attempt to fix the error.
195 
196     // It's difficult to comment out a line in a triple quoted string, so this is used instead
197     // to ignore specific fields. A comment is required to explain why a field was ignored.
198     private fun Any?.ignored(comment: String): String = "IGNORED"
199 
200     protected fun ApplicationInfo.dumpToString() = """
201             appComponentFactory=${this.appComponentFactory}
202             backupAgentName=${this.backupAgentName}
203             banner=${this.banner}
204             category=${this.category}
205             classLoaderName=${this.classLoaderName}
206             className=${this.className}
207             compatibleWidthLimitDp=${this.compatibleWidthLimitDp}
208             compileSdkVersion=${this.compileSdkVersion}
209             compileSdkVersionCodename=${this.compileSdkVersionCodename}
210             credentialProtectedDataDir=${this.credentialProtectedDataDir
211             .ignored("Deferred pre-R, but assigned immediately in R")}
212             crossProfile=${this.crossProfile.ignored("Added in R")}
213             dataDir=${this.dataDir.ignored("Deferred pre-R, but assigned immediately in R")}
214             descriptionRes=${this.descriptionRes}
215             deviceProtectedDataDir=${this.deviceProtectedDataDir
216             .ignored("Deferred pre-R, but assigned immediately in R")}
217             enabled=${this.enabled}
218             enabledSetting=${this.enabledSetting}
219             flags=${Integer.toBinaryString(this.flags)}
220             fullBackupContent=${this.fullBackupContent}
221             gwpAsanMode=${this.gwpAsanMode.ignored("Added in R")}
222             hiddenUntilInstalled=${this.hiddenUntilInstalled}
223             icon=${this.icon}
224             iconRes=${this.iconRes}
225             installLocation=${this.installLocation}
226             labelRes=${this.labelRes}
227             largestWidthLimitDp=${this.largestWidthLimitDp}
228             logo=${this.logo}
229             longVersionCode=${this.longVersionCode}
230             ${"".ignored("mHiddenApiPolicy is a private field")}
231             manageSpaceActivityName=${this.manageSpaceActivityName}
232             maxAspectRatio=${this.maxAspectRatio}
233             metaData=${this.metaData.dumpToString()}
234             minAspectRatio=${this.minAspectRatio}
235             minSdkVersion=${this.minSdkVersion}
236             name=${this.name}
237             nativeLibraryDir=${this.nativeLibraryDir}
238             nativeLibraryRootDir=${this.nativeLibraryRootDir}
239             nativeLibraryRootRequiresIsa=${this.nativeLibraryRootRequiresIsa}
240             networkSecurityConfigRes=${this.networkSecurityConfigRes}
241             nonLocalizedLabel=${
242                 // Per b/184574333, v1 mistakenly trimmed the label. v2 fixed this, but for test
243                 // comparison, trim both so they can be matched.
244                 this.nonLocalizedLabel?.trim()
245             }
246             packageName=${this.packageName}
247             permission=${this.permission}
248             primaryCpuAbi=${this.primaryCpuAbi}
249             privateFlags=${Integer.toBinaryString(this.privateFlags)}
250             processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")}
251             publicSourceDir=${this.publicSourceDir
252             .ignored("Deferred pre-R, but assigned immediately in R")}
253             requiresSmallestWidthDp=${this.requiresSmallestWidthDp}
254             resourceDirs=${this.resourceDirs?.contentToString()}
255             overlayPaths=${this.overlayPaths?.contentToString()}
256             roundIconRes=${this.roundIconRes}
257             scanPublicSourceDir=${this.scanPublicSourceDir
258             .ignored("Deferred pre-R, but assigned immediately in R")}
259             scanSourceDir=${this.scanSourceDir
260             .ignored("Deferred pre-R, but assigned immediately in R")}
261             seInfo=${this.seInfo}
262             seInfoUser=${this.seInfoUser}
263             secondaryCpuAbi=${this.secondaryCpuAbi}
264             secondaryNativeLibraryDir=${this.secondaryNativeLibraryDir}
265             sharedLibraryFiles=${this.sharedLibraryFiles?.contentToString()}
266             sharedLibraryInfos=${this.sharedLibraryInfos}
267             showUserIcon=${this.showUserIcon}
268             sourceDir=${this.sourceDir
269             .ignored("Deferred pre-R, but assigned immediately in R")}
270             splitClassLoaderNames=${this.splitClassLoaderNames?.contentToString()}
271             splitDependencies=${this.splitDependencies.dumpToString()}
272             splitNames=${this.splitNames?.contentToString()}
273             splitPublicSourceDirs=${this.splitPublicSourceDirs?.contentToString()}
274             splitSourceDirs=${this.splitSourceDirs?.contentToString()}
275             storageUuid=${this.storageUuid}
276             targetSandboxVersion=${this.targetSandboxVersion}
277             targetSdkVersion=${this.targetSdkVersion}
278             taskAffinity=${this.taskAffinity}
279             theme=${this.theme}
280             uiOptions=${this.uiOptions}
281             uid=${this.uid}
282             versionCode=${this.versionCode}
283             volumeUuid=${this.volumeUuid}
284             zygotePreloadName=${this.zygotePreloadName}
285             """.trimIndent()
286 
287     protected fun FeatureInfo.dumpToString() = """
288             flags=${Integer.toBinaryString(this.flags)}
289             name=${this.name}
290             reqGlEsVersion=${this.reqGlEsVersion}
291             version=${this.version}
292             """.trimIndent()
293 
294     protected fun InstrumentationInfo.dumpToString() = """
295             banner=${this.banner}
296             credentialProtectedDataDir=${this.credentialProtectedDataDir}
297             dataDir=${this.dataDir}
298             deviceProtectedDataDir=${this.deviceProtectedDataDir}
299             functionalTest=${this.functionalTest}
300             handleProfiling=${this.handleProfiling}
301             icon=${this.icon}
302             labelRes=${this.labelRes}
303             logo=${this.logo}
304             metaData=${this.metaData}
305             name=${this.name}
306             nativeLibraryDir=${this.nativeLibraryDir}
307             nonLocalizedLabel=${
308                 // Per b/184574333, v1 mistakenly trimmed the label. v2 fixed this, but for test
309                 // comparison, trim both so they can be matched.
310                 this.nonLocalizedLabel?.trim()
311             }
312             packageName=${this.packageName}
313             primaryCpuAbi=${this.primaryCpuAbi}
314             publicSourceDir=${this.publicSourceDir}
315             secondaryCpuAbi=${this.secondaryCpuAbi}
316             secondaryNativeLibraryDir=${this.secondaryNativeLibraryDir}
317             showUserIcon=${this.showUserIcon}
318             sourceDir=${this.sourceDir}
319             splitDependencies=${this.splitDependencies.dumpToString()}
320             splitNames=${this.splitNames?.contentToString()}
321             splitPublicSourceDirs=${this.splitPublicSourceDirs?.contentToString()}
322             splitSourceDirs=${this.splitSourceDirs?.contentToString()}
323             targetPackage=${this.targetPackage}
324             targetProcesses=${this.targetProcesses}
325             """.trimIndent()
326 
327     protected fun ActivityInfo.dumpToString() = """
328             banner=${this.banner}
329             colorMode=${this.colorMode}
330             configChanges=${this.configChanges}
331             descriptionRes=${this.descriptionRes}
332             directBootAware=${this.directBootAware}
333             documentLaunchMode=${this.documentLaunchMode
334             .ignored("Update for fixing b/128526493 and the testing is no longer valid")}
335             enabled=${this.enabled}
336             exported=${this.exported}
337             flags=${Integer.toBinaryString(this.flags)}
338             icon=${this.icon}
339             labelRes=${this.labelRes}
340             launchMode=${this.launchMode}
341             launchToken=${this.launchToken}
342             lockTaskLaunchMode=${this.lockTaskLaunchMode}
343             logo=${this.logo}
344             maxRecents=${this.maxRecents}
345             metaData=${this.metaData.dumpToString()}
346             name=${this.name}
347             nonLocalizedLabel=${
348                 // Per b/184574333, v1 mistakenly trimmed the label. v2 fixed this, but for test
349                 // comparison, trim both so they can be matched.
350                 this.nonLocalizedLabel?.trim()
351             }
352             packageName=${this.packageName}
353             parentActivityName=${this.parentActivityName}
354             permission=${this.permission}
355             persistableMode=${this.persistableMode.ignored("Could be dropped pre-R, fixed in R")}
356             privateFlags=${
357                 // Strip flag added in S
358                 this.privateFlags and (ActivityInfo.PRIVATE_FLAG_HOME_TRANSITION_SOUND.inv())
359             }
360             processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")}
361             requestedVrComponent=${this.requestedVrComponent}
362             resizeMode=${this.resizeMode}
363             rotationAnimation=${this.rotationAnimation}
364             screenOrientation=${this.screenOrientation}
365             showUserIcon=${this.showUserIcon}
366             softInputMode=${this.softInputMode}
367             splitName=${this.splitName}
368             targetActivity=${this.targetActivity}
369             taskAffinity=${this.taskAffinity}
370             theme=${this.theme}
371             uiOptions=${this.uiOptions}
372             windowLayout=${this.windowLayout?.dumpToString()}
373             """.trimIndent()
374 
375     protected fun ActivityInfo.WindowLayout.dumpToString() = """
376             gravity=${this.gravity}
377             height=${this.height}
378             heightFraction=${this.heightFraction}
379             minHeight=${this.minHeight}
380             minWidth=${this.minWidth}
381             width=${this.width}
382             widthFraction=${this.widthFraction}
383             """.trimIndent()
384 
385     protected fun PermissionInfo.dumpToString() = """
386             backgroundPermission=${this.backgroundPermission}
387             banner=${this.banner}
388             descriptionRes=${this.descriptionRes}
389             flags=${Integer.toBinaryString(this.flags)}
390             group=${this.group}
391             icon=${this.icon}
392             labelRes=${this.labelRes}
393             logo=${this.logo}
394             metaData=${this.metaData.dumpToString()}
395             name=${this.name}
396             nonLocalizedDescription=${this.nonLocalizedDescription}
397             nonLocalizedLabel=${
398                 // Per b/184574333, v1 mistakenly trimmed the label. v2 fixed this, but for test
399                 // comparison, trim both so they can be matched.
400                 this.nonLocalizedLabel?.trim()
401             }
402             packageName=${this.packageName}
403             protectionLevel=${this.protectionLevel}
404             requestRes=${this.requestRes}
405             showUserIcon=${this.showUserIcon}
406             """.trimIndent()
407 
408     protected fun ProviderInfo.dumpToString() = """
409             applicationInfo=${this.applicationInfo.ignored("Already checked")}
410             authority=${this.authority}
411             banner=${this.banner}
412             descriptionRes=${this.descriptionRes}
413             directBootAware=${this.directBootAware}
414             enabled=${this.enabled}
415             exported=${this.exported}
416             flags=${Integer.toBinaryString(this.flags)}
417             forceUriPermissions=${this.forceUriPermissions}
418             grantUriPermissions=${this.grantUriPermissions}
419             icon=${this.icon}
420             initOrder=${this.initOrder}
421             isSyncable=${this.isSyncable}
422             labelRes=${this.labelRes}
423             logo=${this.logo}
424             metaData=${this.metaData.dumpToString()}
425             multiprocess=${this.multiprocess}
426             name=${this.name}
427             nonLocalizedLabel=${
428                 // Per b/184574333, v1 mistakenly trimmed the label. v2 fixed this, but for test
429                 // comparison, trim both so they can be matched.
430                 this.nonLocalizedLabel?.trim()
431             }
432             packageName=${this.packageName}
433             pathPermissions=${this.pathPermissions?.joinToString {
434         "readPermission=${it.readPermission}\nwritePermission=${it.writePermission}"
435     }}
436             processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")}
437             readPermission=${this.readPermission}
438             showUserIcon=${this.showUserIcon}
439             splitName=${this.splitName}
440             uriPermissionPatterns=${this.uriPermissionPatterns?.contentToString()}
441             writePermission=${this.writePermission}
442             """.trimIndent()
443 
444     protected fun ServiceInfo.dumpToString() = """
445             applicationInfo=${this.applicationInfo.ignored("Already checked")}
446             banner=${this.banner}
447             descriptionRes=${this.descriptionRes}
448             directBootAware=${this.directBootAware}
449             enabled=${this.enabled}
450             exported=${this.exported}
451             flags=${Integer.toBinaryString(this.flags)}
452             icon=${this.icon}
453             labelRes=${this.labelRes}
454             logo=${this.logo}
455             mForegroundServiceType"${this.mForegroundServiceType}
456             metaData=${this.metaData.dumpToString()}
457             name=${this.name}
458             nonLocalizedLabel=${
459                 // Per b/184574333, v1 mistakenly trimmed the label. v2 fixed this, but for test
460                 // comparison, trim both so they can be matched.
461                 this.nonLocalizedLabel?.trim()
462             }
463             packageName=${this.packageName}
464             permission=${this.permission}
465             processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")}
466             showUserIcon=${this.showUserIcon}
467             splitName=${this.splitName}
468             """.trimIndent()
469 
470     protected fun ConfigurationInfo.dumpToString() = """
471             reqGlEsVersion=${this.reqGlEsVersion}
472             reqInputFeatures=${this.reqInputFeatures}
473             reqKeyboardType=${this.reqKeyboardType}
474             reqNavigation=${this.reqNavigation}
475             reqTouchScreen=${this.reqTouchScreen}
476             """.trimIndent()
477 
478     protected fun PackageInfo.dumpToString() = """
479             activities=${this.activities?.joinToString { it.dumpToString() }
480             .ignored("Checked separately in test")}
481             applicationInfo=${this.applicationInfo.dumpToString()
482             .ignored("Checked separately in test")}
483             baseRevisionCode=${this.baseRevisionCode}
484             compileSdkVersion=${this.compileSdkVersion}
485             compileSdkVersionCodename=${this.compileSdkVersionCodename}
486             configPreferences=${this.configPreferences?.joinToString { it.dumpToString() }}
487             coreApp=${this.coreApp}
488             featureGroups=${this.featureGroups?.joinToString {
489         it.features?.joinToString { featureInfo -> featureInfo.dumpToString() }.orEmpty()
490     }}
491             firstInstallTime=${this.firstInstallTime}
492             gids=${gids?.contentToString()}
493             installLocation=${this.installLocation}
494             instrumentation=${instrumentation?.joinToString { it.dumpToString() }}
495             isApex=${this.isApex}
496             isStub=${this.isStub}
497             lastUpdateTime=${this.lastUpdateTime}
498             mOverlayIsStatic=${this.mOverlayIsStatic}
499             overlayCategory=${this.overlayCategory}
500             overlayPriority=${this.overlayPriority}
501             overlayTarget=${this.overlayTarget}
502             packageName=${this.packageName}
503             permissions=${this.permissions?.joinToString { it.dumpToString() }}
504             providers=${this.providers?.joinToString { it.dumpToString() }
505             .ignored("Checked separately in test")}
506             receivers=${this.receivers?.joinToString { it.dumpToString() }
507             .ignored("Checked separately in test")}
508             reqFeatures=${this.reqFeatures?.joinToString { it.dumpToString() }}
509             requestedPermissions=${this.requestedPermissions?.contentToString()}
510             requestedPermissionsFlags=${
511                 this.requestedPermissionsFlags?.map {
512                     // Newer flags are stripped
513                     it and (PackageInfo.REQUESTED_PERMISSION_REQUIRED
514                             or PackageInfo.REQUESTED_PERMISSION_GRANTED)
515                 }?.joinToString()
516             }
517             requiredAccountType=${this.requiredAccountType}
518             requiredForAllUsers=${this.requiredForAllUsers}
519             restrictedAccountType=${this.restrictedAccountType}
520             services=${this.services?.joinToString { it.dumpToString() }
521             .ignored("Checked separately in test")}
522             sharedUserId=${this.sharedUserId}
523             sharedUserLabel=${this.sharedUserLabel}
524             signatures=${this.signatures?.joinToString { it.toCharsString() }}
525             signingInfo=${this.signingInfo?.signingCertificateHistory
526             ?.joinToString { it.toCharsString() }.orEmpty()}
527             splitNames=${this.splitNames?.contentToString()}
528             splitRevisionCodes=${this.splitRevisionCodes?.contentToString()}
529             targetOverlayableName=${this.targetOverlayableName}
530             versionCode=${this.versionCode}
531             versionCodeMajor=${this.versionCodeMajor}
532             versionName=${this.versionName}
533             """.trimIndent()
534 
535     private fun Bundle?.dumpToString() = this?.keySet()?.associateWith { get(it) }?.toString()
536 
537     private fun <T> SparseArray<T>?.dumpToString(): String {
538         if (this == null) {
539             return "EMPTY"
540         }
541 
542         val list = mutableListOf<Pair<Int, T>>()
543         for (index in (0 until size())) {
544             list += keyAt(index) to valueAt(index)
545         }
546         return list.toString()
547     }
548 }
549