Skip to content

Commit

Permalink
Fix various crashes (readium#484)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu authored Apr 8, 2024
1 parent 2786fb4 commit 975e8af
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 24 deletions.
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

0 comments on commit 975e8af

Please sign in to comment.