Skip to content

Commit 975e8af

Browse files
authored
Fix various crashes (#484)
1 parent 2786fb4 commit 975e8af

File tree

13 files changed

+128
-24
lines changed

13 files changed

+128
-24
lines changed

.idea/.gitignore

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ androidx-lifecycle = "2.7.0"
3030
androidx-lifecycle-extensions = "2.2.0"
3131
androidx-media = "1.7.0"
3232
androidx-media2 = "1.3.0"
33-
androidx-media3 = "1.3.0-rc01"
33+
androidx-media3 = "1.3.0"
3434
androidx-navigation = "2.7.6"
3535
androidx-paging = "3.2.1"
3636
androidx-recyclerview = "1.3.2"

readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory<PsPd
4747
} catch (e: InvalidSignatureException) {
4848
Try.failure(ReadError.Decoding(ThrowableError(e)))
4949
} catch (e: IOException) {
50-
Try.failure(dataProvider.error!!)
50+
Try.failure(
51+
dataProvider.error
52+
?: ReadError.UnsupportedOperation(ThrowableError(IllegalStateException(e)))
53+
)
5154
}
5255
}
5356
}

readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt

+18-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.readium.r2.shared.extensions.inflate
1414
import org.readium.r2.shared.extensions.requireLengthFitInt
1515
import org.readium.r2.shared.publication.encryption.Encryption
1616
import org.readium.r2.shared.util.DebugError
17+
import org.readium.r2.shared.util.ThrowableError
1718
import org.readium.r2.shared.util.Try
1819
import org.readium.r2.shared.util.Url
1920
import org.readium.r2.shared.util.data.ReadError
@@ -266,11 +267,27 @@ private suspend fun LcpLicense.decryptFully(
266267

267268
// Removes the padding.
268269
val padding = bytes.last().toInt()
270+
if (padding !in bytes.indices) {
271+
return Try.failure(
272+
ReadError.Decoding(
273+
DebugError(
274+
"The padding length of the encrypted resource is incorrect: $padding / ${bytes.size}"
275+
)
276+
)
277+
)
278+
}
269279
bytes = bytes.copyOfRange(0, bytes.size - padding)
270280

271-
// If the ressource was compressed using deflate, inflates it.
281+
// If the resource was compressed using deflate, inflates it.
272282
if (isDeflated) {
273283
bytes = bytes.inflate(nowrap = true)
284+
.getOrElse {
285+
return Try.failure(
286+
ReadError.Decoding(
287+
DebugError("Cannot deflate the decrypted resource", ThrowableError(it))
288+
)
289+
)
290+
}
274291
}
275292

276293
Try.success(bytes)

readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt

+6-4
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,12 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>,
322322
}
323323

324324
coroutineScope.launch {
325-
playbackJob?.cancel()
326-
playbackJob?.join()
327-
utteranceMutable.value = utteranceMutable.value.copy(range = null)
328-
playIfReadyAndNotPaused()
325+
mutex.withLock {
326+
playbackJob?.cancel()
327+
playbackJob?.join()
328+
utteranceMutable.value = utteranceMutable.value.copy(range = null)
329+
playIfReadyAndNotPaused()
330+
}
329331
}
330332
}
331333

readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt

+17-11
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ package org.readium.r2.shared.extensions
1111

1212
import java.io.ByteArrayOutputStream
1313
import java.security.MessageDigest
14+
import java.util.zip.DataFormatException
1415
import java.util.zip.Inflater
1516
import org.readium.r2.shared.InternalReadiumApi
17+
import org.readium.r2.shared.util.Try
1618
import timber.log.Timber
1719

1820
/**
@@ -21,18 +23,22 @@ import timber.log.Timber
2123
* @param nowrap If true then support GZIP compatible compression, see the documentation of [Inflater]
2224
*/
2325
@InternalReadiumApi
24-
public fun ByteArray.inflate(nowrap: Boolean = false, bufferSize: Int = 32 * 1024 /* 32 KB */): ByteArray =
25-
ByteArrayOutputStream().use { output ->
26-
val inflater = Inflater(nowrap)
27-
inflater.setInput(this)
28-
29-
val buffer = ByteArray(bufferSize)
30-
while (!inflater.finished()) {
31-
val count = inflater.inflate(buffer)
32-
output.write(buffer, 0, count)
33-
}
26+
public fun ByteArray.inflate(nowrap: Boolean = false, bufferSize: Int = 32 * 1024 /* 32 KB */): Try<ByteArray, DataFormatException> =
27+
try {
28+
ByteArrayOutputStream().use { output ->
29+
val inflater = Inflater(nowrap)
30+
inflater.setInput(this)
3431

35-
output.toByteArray()
32+
val buffer = ByteArray(bufferSize)
33+
while (!inflater.finished()) {
34+
val count = inflater.inflate(buffer)
35+
output.write(buffer, 0, count)
36+
}
37+
38+
Try.success(output.toByteArray())
39+
}
40+
} catch (e: DataFormatException) {
41+
Try.failure(e)
3642
}
3743

3844
/** Computes the MD5 hash of the byte array. */

readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ public sealed class ContentResolverError(
2525
public constructor(exception: Exception) : this(ThrowableError(exception))
2626
}
2727

28+
public class Forbidden(
29+
cause: Error?
30+
) : ContentResolverError("You are not allowed to access this file.", cause) {
31+
32+
public constructor(exception: Exception) : this(ThrowableError(exception))
33+
}
34+
2835
public class NotAvailable(
2936
cause: Error? = null
3037
) : ContentResolverError("Content Provider recently crashed.", cause) {

readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt

+2
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ public class ContentResource(
150150
failure(ReadError.Access(ContentResolverError.FileNotFound(e)))
151151
} catch (e: IOException) {
152152
failure(ReadError.Access(ContentResolverError.IO(e)))
153+
} catch (e: SecurityException) {
154+
failure(ReadError.Access(ContentResolverError.Forbidden(e)))
153155
} catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM.
154156
failure(ReadError.OutOfMemory(e))
155157
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.readium.r2.streamer.extensions
2+
3+
import com.mcxiaoke.koi.HASH
4+
import com.mcxiaoke.koi.ext.toHexBytes
5+
import org.readium.r2.shared.extensions.tryOrNull
6+
7+
/**
8+
* Computes the SHA-1 hash of a string.
9+
*/
10+
internal fun String.sha1(): String =
11+
HASH.sha1(this)
12+
13+
/**
14+
* Converts an hexadecimal string (e.g. 8ad5078e) to a byte array.
15+
*/
16+
internal fun String.toHexByteArray(): ByteArray? {
17+
// Only even-length strings can be converted to an Hex byte array, otherwise it crashes
18+
// with StringIndexOutOfBoundsException.
19+
if (isEmpty() || !hasEvenLength() || !isHexadecimal()) {
20+
return null
21+
}
22+
return tryOrNull { toHexBytes() }
23+
}
24+
25+
private fun String.hasEvenLength(): Boolean =
26+
length % 2 == 0
27+
28+
private fun String.isHexadecimal(): Boolean =
29+
all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }

readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt

+10-5
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66

77
package org.readium.r2.streamer.parser.epub
88

9-
import com.mcxiaoke.koi.HASH
10-
import com.mcxiaoke.koi.ext.toHexBytes
119
import kotlin.experimental.xor
1210
import org.readium.r2.shared.publication.encryption.Encryption
11+
import org.readium.r2.shared.util.Try
1312
import org.readium.r2.shared.util.Url
13+
import org.readium.r2.shared.util.data.ReadError
1414
import org.readium.r2.shared.util.data.ReadTry
1515
import org.readium.r2.shared.util.resource.Resource
1616
import org.readium.r2.shared.util.resource.TransformingResource
1717
import org.readium.r2.shared.util.resource.flatMap
18+
import org.readium.r2.streamer.extensions.sha1
19+
import org.readium.r2.streamer.extensions.toHexByteArray
1820

1921
/**
2022
* Deobfuscates fonts according to https://www.w3.org/TR/epub-33/#sec-font-obfuscation
@@ -49,9 +51,13 @@ internal class EpubDeobfuscator(
4951
val obfuscationLength: Int = algorithm2length[algorithm]
5052
?: return@map bytes
5153

52-
val obfuscationKey: ByteArray = when (algorithm) {
54+
val obfuscationKey: ByteArray? = when (algorithm) {
5355
"http://ns.adobe.com/pdf/enc#RC" -> getHashKeyAdobe(pubId)
54-
else -> HASH.sha1(pubId).toHexBytes()
56+
else -> pubId.sha1()
57+
}.toHexByteArray()
58+
59+
if (obfuscationKey == null || obfuscationKey.isEmpty()) {
60+
return Try.failure(ReadError.Decoding("The obfuscation key is not valid."))
5561
}
5662

5763
deobfuscate(
@@ -78,5 +84,4 @@ internal class EpubDeobfuscator(
7884
private fun getHashKeyAdobe(pubId: String) =
7985
pubId.replace("urn:uuid:", "")
8086
.replace("-", "")
81-
.toHexBytes()
8287
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.readium.r2.streamer.extensions
2+
3+
import kotlin.test.assertContentEquals
4+
import kotlin.test.assertNull
5+
import org.junit.Test
6+
7+
class StringExtTest {
8+
9+
@Test
10+
fun `convert an hexadecimal string to a byte array`() {
11+
assertNull("".toHexByteArray())
12+
// Forbids odd-length strings.
13+
assertNull("8".toHexByteArray())
14+
assertNull("8ad".toHexByteArray())
15+
// Forbids character outside 0-f range.
16+
assertNull("8y".toHexByteArray())
17+
18+
assertContentEquals(byteArrayOf(0x8a), "8a".toHexByteArray())
19+
assertContentEquals(byteArrayOf(0x8a), "8A".toHexByteArray())
20+
assertContentEquals(byteArrayOf(0x8a, 0xd5, 0x07, 0x8e), "8ad5078e".toHexByteArray())
21+
}
22+
23+
private fun byteArrayOf(vararg bytes: Int): ByteArray {
24+
return bytes.map { it.toByte() }.toByteArray()
25+
}
26+
}

test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ fun HttpError.toUserError(): UserError = when (this) {
6262

6363
fun FileSystemError.toUserError(): UserError = when (this) {
6464
is FileSystemError.Forbidden -> UserError(
65-
R.string.publication_error_filesystem_unexpected,
65+
R.string.publication_error_filesystem_forbidden,
6666
cause = this
6767
)
6868
is FileSystemError.IO -> UserError(
@@ -88,5 +88,9 @@ fun ContentResolverError.toUserError(): UserError = when (this) {
8888
R.string.publication_error_filesystem_unexpected,
8989
cause = this
9090
)
91+
is ContentResolverError.Forbidden -> UserError(
92+
R.string.publication_error_filesystem_forbidden,
93+
cause = this
94+
)
9195
is ContentResolverError.NotAvailable -> UserError(R.string.error_unexpected, cause = this)
9296
}

test-app/src/main/res/values/strings.xml

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
<string name="publication_error_network_ssl_handshake">A SSL error occurred.</string>
114114
<string name="publication_error_network_unexpected">An unexpected network error occurred.</string>
115115
<string name="publication_error_filesystem_not_found">A file has not been found.</string>
116+
<string name="publication_error_filesystem_forbidden">You are not allowed to access the file.</string>
116117
<string name="publication_error_filesystem_unexpected">An unexpected filesystem error occurred.</string>
117118
<string name="publication_error_filesystem_insufficient_space">There is not enough space left on the device.</string>
118119
<string name="publication_error_incorrect_credentials">Provided credentials were incorrect</string>

0 commit comments

Comments
 (0)