From 5b59df7cfae58d9f33b2e7a48d483eb5aedea99b Mon Sep 17 00:00:00 2001 From: Lauris Date: Mon, 16 Feb 2026 23:19:32 +0100 Subject: [PATCH 1/3] Fix crash when JSONObject is backed by immutable Kotlin map 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. --- .../main/java/org/readium/r2/shared/publication/Locator.kt | 4 ++-- .../main/java/org/readium/r2/shared/publication/Metadata.kt | 2 +- .../main/java/org/readium/r2/shared/publication/Properties.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt index 216d072887..7e4bb80271 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt @@ -68,7 +68,7 @@ public data class Locator( val otherLocations: @WriteWith Map = emptyMap(), ) : JSONable, Parcelable { - override fun toJSON(): JSONObject = JSONObject(otherLocations).apply { + override fun toJSON(): JSONObject = JSONObject(HashMap(otherLocations)).apply { putIfNotEmpty("fragments", fragments) put("progression", progression) put("position", position) @@ -285,7 +285,7 @@ public data class LocatorCollection( */ val title: String? get() = localizedTitle?.string - override fun toJSON(): JSONObject = JSONObject(otherMetadata).apply { + override fun toJSON(): JSONObject = JSONObject(HashMap(otherMetadata)).apply { putIfNotEmpty("title", localizedTitle) putOpt("numberOfItems", numberOfItems) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt index b36156d7a4..ac238612d0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt @@ -179,7 +179,7 @@ public data class Metadata( /** * Serializes a [Metadata] to its RWPM JSON representation. */ - override fun toJSON(): JSONObject = JSONObject(otherMetadata).apply { + override fun toJSON(): JSONObject = JSONObject(HashMap(otherMetadata)).apply { put("identifier", identifier) put("@type", type) putIfNotEmpty("conformsTo", conformsTo.map { it.uri }) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt index 96fc3108b3..93313b480e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt @@ -34,7 +34,7 @@ public data class Properties( /** * Serializes a [Properties] to its RWPM JSON representation. */ - override fun toJSON(): JSONObject = JSONObject(otherProperties) + override fun toJSON(): JSONObject = JSONObject(HashMap(otherProperties)) /** * Makes a copy of this [Properties] after merging in the given additional other [properties]. From df0a5b368a20737721deb7777c0b93faf8f09fff Mon Sep 17 00:00:00 2001 From: Lauris Date: Tue, 17 Feb 2026 14:29:49 +0100 Subject: [PATCH 2/3] Use toMutableMap() instead of HashMap() for clarity Also add a comment explaining why mutability is needed. --- .../main/java/org/readium/r2/shared/publication/Locator.kt | 6 ++++-- .../main/java/org/readium/r2/shared/publication/Metadata.kt | 2 +- .../java/org/readium/r2/shared/publication/Properties.kt | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt index 7e4bb80271..485f9e3cd0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt @@ -68,7 +68,9 @@ public data class Locator( val otherLocations: @WriteWith Map = emptyMap(), ) : JSONable, Parcelable { - override fun toJSON(): JSONObject = JSONObject(HashMap(otherLocations)).apply { + // Some versions of org.json's JSONObject(Map) store the reference directly + // without copying, so the map must be mutable. + override fun toJSON(): JSONObject = JSONObject(otherLocations.toMutableMap()).apply { putIfNotEmpty("fragments", fragments) put("progression", progression) put("position", position) @@ -285,7 +287,7 @@ public data class LocatorCollection( */ val title: String? get() = localizedTitle?.string - override fun toJSON(): JSONObject = JSONObject(HashMap(otherMetadata)).apply { + override fun toJSON(): JSONObject = JSONObject(otherMetadata.toMutableMap()).apply { putIfNotEmpty("title", localizedTitle) putOpt("numberOfItems", numberOfItems) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt index ac238612d0..866e7d2135 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt @@ -179,7 +179,7 @@ public data class Metadata( /** * Serializes a [Metadata] to its RWPM JSON representation. */ - override fun toJSON(): JSONObject = JSONObject(HashMap(otherMetadata)).apply { + override fun toJSON(): JSONObject = JSONObject(otherMetadata.toMutableMap()).apply { put("identifier", identifier) put("@type", type) putIfNotEmpty("conformsTo", conformsTo.map { it.uri }) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt index 93313b480e..8c1f3694a6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt @@ -34,7 +34,7 @@ public data class Properties( /** * Serializes a [Properties] to its RWPM JSON representation. */ - override fun toJSON(): JSONObject = JSONObject(HashMap(otherProperties)) + override fun toJSON(): JSONObject = JSONObject(otherProperties.toMutableMap()) /** * Makes a copy of this [Properties] after merging in the given additional other [properties]. From 160edaf0ca882a949b3fd437afbd000c1b69bc98 Mon Sep 17 00:00:00 2001 From: qnga <32197639+qnga@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:57:14 +0100 Subject: [PATCH 3/3] Update readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt --- .../src/main/java/org/readium/r2/shared/publication/Locator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt index 485f9e3cd0..8aa94d6bb0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt @@ -69,7 +69,8 @@ public data class Locator( ) : JSONable, Parcelable { // Some versions of org.json's JSONObject(Map) store the reference directly - // without copying, so the map must be mutable. + // without copying, so the map must be mutable. And even on other versions, + // R8 optimizations can make things go like this. override fun toJSON(): JSONObject = JSONObject(otherLocations.toMutableMap()).apply { putIfNotEmpty("fragments", fragments) put("progression", progression)