Fix crash when JSONObject is backed by immutable Kotlin map#755
Fix crash when JSONObject is backed by immutable Kotlin map#755qnga merged 3 commits intoreadium:developfrom
Conversation
JSONObject(otherLocations) and similar calls pass a Kotlin emptyMap() directly to the JSONObject constructor. When R8 optimizes the app, it can keep the immutable map as the backing store instead of copying into a HashMap. Any subsequent remove() or put() on that JSONObject then throws UnsupportedOperationException. Wrapping the map in HashMap() ensures the JSONObject always has a mutable backing map regardless of R8 optimization.
|
Isn't this a R8 bug? It should not use an immutable collection where a mutable one is expected and is actually mutated. That being said, I have nothing against a workaround. |
|
You're right that R8 is being overly aggressive here it sees the That said, there's a compounding factor: the bundled Either way, wrapping in |
So I guess it casts the Map to a MutableMap to be able to put new keys into it?
What do you use instead? As I guess you're being using just another version, have you figured out which dependency pulls transitively the |
|
By the way, given your explanation, I agree on the need for a fix. Would you mind using |
|
The 20090211 version stores the map reference directly in its internal I exclude it globally with |
|
Sure, |
Also add a comment explaining why mutability is needed.
There was a problem hiding this comment.
Pull request overview
This PR prevents toJSON() crashes under R8/minification when JSONObject(Map) ends up backed by Kotlin’s immutable emptyMap() (leading to UnsupportedOperationException when remove() is invoked by putIfNotEmpty).
Changes:
- Wrap
otherProperties/otherMetadata/otherLocationswithtoMutableMap()before passing toJSONObject(Map)to guarantee a mutable backing map. - Add an explanatory comment in
Locator.Locations.toJSON()about the mutability requirement.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt | Ensure Properties.toJSON() always builds JSONObject from a mutable map. |
| readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt | Ensure Metadata.toJSON() starts from a mutable map before putIfNotEmpty calls. |
| readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt | Ensure locator-related toJSON() methods start from mutable maps; add context comment. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
When building with R8 (minification enabled),
toJSON()methods inLocator.Locations,Locator.Metadata,Metadata, andPropertiescrash with:The issue is that these classes have
otherLocations/otherMetadata/otherPropertiesfields that default toemptyMap(), which is Kotlin's immutable empty map singleton. When this gets passed to theJSONObject(Map)constructor, R8 can optimize away the internal copy into aLinkedHashMap(since the map is empty, the copy loop is a no-op and R8 eliminates it). This leaves the JSONObject backed by the immutableEmptyMap, and then any call toputIfNotEmptythat triggersremove()crashes.The fix just wraps the map in
HashMap()before passing it to the JSONObject constructor so it's always mutable.Hit this in my app targeting API 36 with AGP 8.13 / R8 full mode, on the
notifyCurrentLocationflow inEpubNavigatorFragment.