Skip to content

Fix various crashes #484

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 8, 2024
2 changes: 2 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory<PsPd
} catch (e: InvalidSignatureException) {
Try.failure(ReadError.Decoding(ThrowableError(e)))
} catch (e: IOException) {
Try.failure(dataProvider.error!!)
Try.failure(
dataProvider.error
?: ReadError.UnsupportedOperation(ThrowableError(IllegalStateException(e)))
)
}
}
}
Expand Down
19 changes: 18 additions & 1 deletion readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.readium.r2.shared.extensions.inflate
import org.readium.r2.shared.extensions.requireLengthFitInt
import org.readium.r2.shared.publication.encryption.Encryption
import org.readium.r2.shared.util.DebugError
import org.readium.r2.shared.util.ThrowableError
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.data.ReadError
Expand Down Expand Up @@ -266,11 +267,27 @@ private suspend fun LcpLicense.decryptFully(

// Removes the padding.
val padding = bytes.last().toInt()
if (padding !in bytes.indices) {
return Try.failure(
ReadError.Decoding(
DebugError(
"The padding length of the encrypted resource is incorrect: $padding / ${bytes.size}"
)
)
)
}
bytes = bytes.copyOfRange(0, bytes.size - padding)

// If the ressource was compressed using deflate, inflates it.
// If the resource was compressed using deflate, inflates it.
if (isDeflated) {
bytes = bytes.inflate(nowrap = true)
.getOrElse {
return Try.failure(
ReadError.Decoding(
DebugError("Cannot deflate the decrypted resource", ThrowableError(it))
)
)
}
}

Try.success(bytes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,12 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>,
}

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()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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<ByteArray, DataFormatException> =
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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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' }
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -78,5 +84,4 @@ internal class EpubDeobfuscator(
private fun getHashKeyAdobe(pubId: String) =
pubId.replace("urn:uuid:", "")
.replace("-", "")
.toHexBytes()
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
}
1 change: 1 addition & 0 deletions test-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
<string name="publication_error_network_ssl_handshake">A SSL error occurred.</string>
<string name="publication_error_network_unexpected">An unexpected network error occurred.</string>
<string name="publication_error_filesystem_not_found">A file has not been found.</string>
<string name="publication_error_filesystem_forbidden">You are not allowed to access the file.</string>
<string name="publication_error_filesystem_unexpected">An unexpected filesystem error occurred.</string>
<string name="publication_error_filesystem_insufficient_space">There is not enough space left on the device.</string>
<string name="publication_error_incorrect_credentials">Provided credentials were incorrect</string>
Expand Down