From 975e8af207963cd70b7274a87928e35d59b07a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Mon, 8 Apr 2024 13:05:59 +0200 Subject: [PATCH] Fix various crashes (#484) --- .idea/.gitignore | 2 ++ gradle/libs.versions.toml | 2 +- .../pspdfkit/document/PsPdfKitDocument.kt | 5 +++- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 19 +++++++++++- .../readium/navigator/media/tts/TtsPlayer.kt | 10 ++++--- .../readium/r2/shared/extensions/ByteArray.kt | 28 +++++++++++------- .../util/content/ContentResolverError.kt | 7 +++++ .../r2/shared/util/content/ContentResource.kt | 2 ++ .../r2/streamer/extensions/StringExt.kt | 29 +++++++++++++++++++ .../streamer/parser/epub/EpubDeobfuscator.kt | 15 ++++++---- .../r2/streamer/extensions/StringExtTest.kt | 26 +++++++++++++++++ .../r2/testapp/domain/ReadUserError.kt | 6 +++- test-app/src/main/res/values/strings.xml | 1 + 13 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 readium/streamer/src/main/java/org/readium/r2/streamer/extensions/StringExt.kt create mode 100644 readium/streamer/src/test/java/org/readium/r2/streamer/extensions/StringExtTest.kt diff --git a/.idea/.gitignore b/.idea/.gitignore index 26d33521af..8f00030d59 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,3 +1,5 @@ # Default ignored files /shelf/ /workspace.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8814bc5cae..4f5f7110fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ androidx-lifecycle = "2.7.0" androidx-lifecycle-extensions = "2.2.0" androidx-media = "1.7.0" androidx-media2 = "1.3.0" -androidx-media3 = "1.3.0-rc01" +androidx-media3 = "1.3.0" androidx-navigation = "2.7.6" androidx-paging = "3.2.1" androidx-recyclerview = "1.3.2" diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index 102a79b539..dadcc476f6 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -47,7 +47,10 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory, } coroutineScope.launch { - playbackJob?.cancel() - playbackJob?.join() - utteranceMutable.value = utteranceMutable.value.copy(range = null) - playIfReadyAndNotPaused() + mutex.withLock { + playbackJob?.cancel() + playbackJob?.join() + utteranceMutable.value = utteranceMutable.value.copy(range = null) + playIfReadyAndNotPaused() + } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt index 9606e762e2..559efc460b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt @@ -11,8 +11,10 @@ package org.readium.r2.shared.extensions import java.io.ByteArrayOutputStream import java.security.MessageDigest +import java.util.zip.DataFormatException import java.util.zip.Inflater import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.util.Try import timber.log.Timber /** @@ -21,18 +23,22 @@ import timber.log.Timber * @param nowrap If true then support GZIP compatible compression, see the documentation of [Inflater] */ @InternalReadiumApi -public fun ByteArray.inflate(nowrap: Boolean = false, bufferSize: Int = 32 * 1024 /* 32 KB */): ByteArray = - ByteArrayOutputStream().use { output -> - val inflater = Inflater(nowrap) - inflater.setInput(this) - - val buffer = ByteArray(bufferSize) - while (!inflater.finished()) { - val count = inflater.inflate(buffer) - output.write(buffer, 0, count) - } +public fun ByteArray.inflate(nowrap: Boolean = false, bufferSize: Int = 32 * 1024 /* 32 KB */): Try = + try { + ByteArrayOutputStream().use { output -> + val inflater = Inflater(nowrap) + inflater.setInput(this) - output.toByteArray() + val buffer = ByteArray(bufferSize) + while (!inflater.finished()) { + val count = inflater.inflate(buffer) + output.write(buffer, 0, count) + } + + Try.success(output.toByteArray()) + } + } catch (e: DataFormatException) { + Try.failure(e) } /** Computes the MD5 hash of the byte array. */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt index 38e058eea7..f6140baad5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt @@ -25,6 +25,13 @@ public sealed class ContentResolverError( public constructor(exception: Exception) : this(ThrowableError(exception)) } + public class Forbidden( + cause: Error? + ) : ContentResolverError("You are not allowed to access this file.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + public class NotAvailable( cause: Error? = null ) : ContentResolverError("Content Provider recently crashed.", cause) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt index da202076b7..5bc8e04828 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -150,6 +150,8 @@ public class ContentResource( failure(ReadError.Access(ContentResolverError.FileNotFound(e))) } catch (e: IOException) { failure(ReadError.Access(ContentResolverError.IO(e))) + } catch (e: SecurityException) { + failure(ReadError.Access(ContentResolverError.Forbidden(e))) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. failure(ReadError.OutOfMemory(e)) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/StringExt.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/StringExt.kt new file mode 100644 index 0000000000..2fbc212831 --- /dev/null +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/StringExt.kt @@ -0,0 +1,29 @@ +package org.readium.r2.streamer.extensions + +import com.mcxiaoke.koi.HASH +import com.mcxiaoke.koi.ext.toHexBytes +import org.readium.r2.shared.extensions.tryOrNull + +/** + * Computes the SHA-1 hash of a string. + */ +internal fun String.sha1(): String = + HASH.sha1(this) + +/** + * Converts an hexadecimal string (e.g. 8ad5078e) to a byte array. + */ +internal fun String.toHexByteArray(): ByteArray? { + // Only even-length strings can be converted to an Hex byte array, otherwise it crashes + // with StringIndexOutOfBoundsException. + if (isEmpty() || !hasEvenLength() || !isHexadecimal()) { + return null + } + return tryOrNull { toHexBytes() } +} + +private fun String.hasEvenLength(): Boolean = + length % 2 == 0 + +private fun String.isHexadecimal(): Boolean = + all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt index 0896042ae3..a1e5783505 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt @@ -6,15 +6,17 @@ package org.readium.r2.streamer.parser.epub -import com.mcxiaoke.koi.HASH -import com.mcxiaoke.koi.ext.toHexBytes import kotlin.experimental.xor import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap +import org.readium.r2.streamer.extensions.sha1 +import org.readium.r2.streamer.extensions.toHexByteArray /** * Deobfuscates fonts according to https://www.w3.org/TR/epub-33/#sec-font-obfuscation @@ -49,9 +51,13 @@ internal class EpubDeobfuscator( val obfuscationLength: Int = algorithm2length[algorithm] ?: return@map bytes - val obfuscationKey: ByteArray = when (algorithm) { + val obfuscationKey: ByteArray? = when (algorithm) { "http://ns.adobe.com/pdf/enc#RC" -> getHashKeyAdobe(pubId) - else -> HASH.sha1(pubId).toHexBytes() + else -> pubId.sha1() + }.toHexByteArray() + + if (obfuscationKey == null || obfuscationKey.isEmpty()) { + return Try.failure(ReadError.Decoding("The obfuscation key is not valid.")) } deobfuscate( @@ -78,5 +84,4 @@ internal class EpubDeobfuscator( private fun getHashKeyAdobe(pubId: String) = pubId.replace("urn:uuid:", "") .replace("-", "") - .toHexBytes() } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/StringExtTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/StringExtTest.kt new file mode 100644 index 0000000000..41e520b5e2 --- /dev/null +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/StringExtTest.kt @@ -0,0 +1,26 @@ +package org.readium.r2.streamer.extensions + +import kotlin.test.assertContentEquals +import kotlin.test.assertNull +import org.junit.Test + +class StringExtTest { + + @Test + fun `convert an hexadecimal string to a byte array`() { + assertNull("".toHexByteArray()) + // Forbids odd-length strings. + assertNull("8".toHexByteArray()) + assertNull("8ad".toHexByteArray()) + // Forbids character outside 0-f range. + assertNull("8y".toHexByteArray()) + + assertContentEquals(byteArrayOf(0x8a), "8a".toHexByteArray()) + assertContentEquals(byteArrayOf(0x8a), "8A".toHexByteArray()) + assertContentEquals(byteArrayOf(0x8a, 0xd5, 0x07, 0x8e), "8ad5078e".toHexByteArray()) + } + + private fun byteArrayOf(vararg bytes: Int): ByteArray { + return bytes.map { it.toByte() }.toByteArray() + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt index f0b4c8a7fe..ff8e924793 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -62,7 +62,7 @@ fun HttpError.toUserError(): UserError = when (this) { fun FileSystemError.toUserError(): UserError = when (this) { is FileSystemError.Forbidden -> UserError( - R.string.publication_error_filesystem_unexpected, + R.string.publication_error_filesystem_forbidden, cause = this ) is FileSystemError.IO -> UserError( @@ -88,5 +88,9 @@ fun ContentResolverError.toUserError(): UserError = when (this) { R.string.publication_error_filesystem_unexpected, cause = this ) + is ContentResolverError.Forbidden -> UserError( + R.string.publication_error_filesystem_forbidden, + cause = this + ) is ContentResolverError.NotAvailable -> UserError(R.string.error_unexpected, cause = this) } diff --git a/test-app/src/main/res/values/strings.xml b/test-app/src/main/res/values/strings.xml index 1b5a3a8ff0..906e297cfb 100644 --- a/test-app/src/main/res/values/strings.xml +++ b/test-app/src/main/res/values/strings.xml @@ -113,6 +113,7 @@ A SSL error occurred. An unexpected network error occurred. A file has not been found. + You are not allowed to access the file. An unexpected filesystem error occurred. There is not enough space left on the device. Provided credentials were incorrect