From 8f1a0b9eca20397be04166e7f5bd596022772ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 3 Oct 2023 16:38:13 +0200 Subject: [PATCH 01/11] Fix crash when resetting the PSPDFKit fragment after the state was saved --- .../adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt index 2b36287cab..fb668f2757 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt @@ -162,7 +162,7 @@ public class PsPdfKitDocumentFragment internal constructor( * Recreates the [PdfFragment] with the current settings. */ private fun resetPdfFragment() { - if (view == null) return + if (isStateSaved || view == null) return val doc = viewModel.document ?: return doc.document.pageBinding = settings.readingProgression.pageBinding From e613ec8ffb57a0700cadb0c9e770c97f40957ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Wed, 4 Oct 2023 19:27:01 +0200 Subject: [PATCH 02/11] Improve error reporting --- .../readium/r2/lcp/LcpContentProtection.kt | 9 ++-- .../readium/r2/lcp/LcpPublicationRetriever.kt | 3 +- .../r2/shared/publication/Publication.kt | 14 +++--- .../java/org/readium/r2/shared/util/Error.kt | 45 +++++++------------ .../r2/shared/util/asset/AssetRetriever.kt | 6 ++- .../r2/shared/util/resource/Factories.kt | 6 ++- .../readium/r2/streamer/ParserAssetFactory.kt | 20 ++++----- 7 files changed, 46 insertions(+), 57 deletions(-) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index e62fed24dc..2be61baff9 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -113,7 +113,10 @@ internal class LcpContentProtection( LicenseDocument(it) } catch (e: Exception) { return Try.failure( - Publication.OpenError.InvalidAsset(cause = ThrowableError(e)) + Publication.OpenError.InvalidAsset( + "Failed to read the LCP license document", + cause = ThrowableError(e) + ) ) } } @@ -127,9 +130,7 @@ internal class LcpContentProtection( val url = (link.url() as? AbsoluteUrl) ?: return Try.failure( Publication.OpenError.InvalidAsset( - cause = ThrowableError( - LcpException.Parsing.Url(rel = LicenseDocument.Rel.Publication.value) - ) + "The LCP license document does not contain a valid link to the publication" ) ) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index d90208c6a5..8582f96031 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.launch import org.readium.r2.lcp.license.container.createLicenseContainer import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType @@ -255,7 +256,7 @@ public class LcpPublicationRetriever( listenersForId.forEach { it.onAcquisitionFailed( lcpRequestId, - LcpException.Network(Exception(error.message)) + LcpException.Network(ErrorException(error)) ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index 3afd578a8d..094ab988af 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -27,7 +27,6 @@ import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.search.SearchService -import org.readium.r2.shared.util.BaseError import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError @@ -497,8 +496,10 @@ public class Publication( /** * Errors occurring while opening a Publication. */ - public sealed class OpenError(message: String, cause: Error? = null) : - BaseError(message, cause) { + public sealed class OpenError( + override val message: String, + override val cause: Error? = null + ) : Error { /** * The file format could not be recognized by any parser. @@ -514,12 +515,11 @@ public class Publication( /** * The publication parsing failed with the given underlying error. */ - public class InvalidAsset private constructor( + public class InvalidAsset( message: String, - cause: Error? + cause: Error? = null ) : OpenError(message, cause) { - public constructor(message: String) : this(message, null) - public constructor(cause: Error? = null) : this( + public constructor(cause: Error?) : this( "The asset seems corrupted so the publication cannot be opened.", cause ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index ce365cd1fb..ddb6775f11 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -22,42 +22,27 @@ public interface Error { public val cause: Error? } -/** - * An error caused by the catch of a throwable. - * - * @param throwable the cause Throwable - */ -public class ThrowableError( - public val throwable: Throwable -) : BaseError(throwable.message ?: throwable.toString(), cause = null) - /** * A basic [Error] implementation with a message. */ public class MessageError( - override val message: String -) : BaseError(message, cause = null) + override val message: String, + override val cause: Error? = null +) : Error /** - * A basic implementation of [Error] able to print itself in a structured way. + * An error caused by the catch of a throwable. */ -public abstract class BaseError( - override val message: String, - override val cause: Error? = null +public class ThrowableError( + public val throwable: Throwable ) : Error { - override fun toString(): String { - var desc = "${javaClass.nameWithEnclosingClasses()}: $message" - if (cause != null) { - desc += "\n ${cause.toString().prependIndent(" ")}" - } - return desc - } - - private fun Class<*>.nameWithEnclosingClasses(): String { - var name = simpleName - enclosingClass?.let { - name = "${it.nameWithEnclosingClasses()}.$name" - } - return name - } + override val message: String = throwable.message ?: throwable.toString() + override val cause: Error? = null } + +/** + * A throwable caused by an [Error]. + */ +public class ErrorException( + public val error: Error +) : Exception(error.message) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index ed57d4f8b9..10f0e2c915 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -13,7 +13,6 @@ import android.provider.MediaStore import java.io.File import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.BaseError import org.readium.r2.shared.util.Either import org.readium.r2.shared.util.Error as SharedError import org.readium.r2.shared.util.ThrowableError @@ -61,7 +60,10 @@ public class AssetRetriever( } } - public sealed class Error(message: String, cause: SharedError?) : BaseError(message, cause) { + public sealed class Error( + override val message: String, + override val cause: SharedError? + ) : SharedError { public class SchemeNotSupported( public val scheme: Url.Scheme, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt index 84061ad718..937569fb67 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt @@ -7,7 +7,6 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.BaseError import org.readium.r2.shared.util.Error as SharedError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try @@ -125,7 +124,10 @@ public fun interface ContainerFactory { */ public fun interface ArchiveFactory { - public sealed class Error(message: String, cause: SharedError?) : BaseError(message, cause) { + public sealed class Error( + override val message: String, + override val cause: SharedError? + ) : SharedError { public class FormatNotSupported( cause: SharedError? = null diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 5c1cf41d42..291e87a8c7 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -12,7 +12,6 @@ import org.readium.r2.shared.extensions.addPrefix import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -69,30 +68,29 @@ internal class ParserAssetFactory( asset: org.readium.r2.shared.util.asset.Asset.Resource ): Try { val manifest = asset.resource.readAsRwpm() - .mapFailure { Publication.OpenError.InvalidAsset(ThrowableError(it)) } + .mapFailure { + Publication.OpenError.InvalidAsset( + "Failed to read the publication as a RWPM", + ThrowableError(it) + ) + } .getOrElse { return Try.failure(it) } val baseUrl = manifest.linkWithRel("self")?.href?.resolve() ?: return Try.failure( - Publication.OpenError.InvalidAsset( - MessageError("No self link in the manifest.") - ) + Publication.OpenError.InvalidAsset("No self link in the manifest.") ) if (baseUrl !is AbsoluteUrl) { return Try.failure( - Publication.OpenError.InvalidAsset( - MessageError("Self link is not absolute.") - ) + Publication.OpenError.InvalidAsset("Self link is not absolute.") ) } if (!baseUrl.isHttp) { return Try.failure( - Publication.OpenError.UnsupportedAsset( - "Self link doesn't use the HTTP(S) scheme." - ) + Publication.OpenError.UnsupportedAsset("Self link doesn't use the HTTP(S) scheme.") ) } From daedee76b622d78f89129f171f11b047729c165d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Wed, 4 Oct 2023 19:34:06 +0200 Subject: [PATCH 03/11] Add the `url` in `Resource.Exception` --- .../pspdfkit/document/PsPdfKitDocument.kt | 8 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 22 +++--- .../org/readium/r2/lcp/LcpDecryptorTest.kt | 2 +- .../services/ContentProtectionService.kt | 9 ++- .../publication/services/CoverService.kt | 2 +- .../r2/shared/util/http/HttpContainer.kt | 1 + .../r2/shared/util/http/HttpResource.kt | 24 +++--- .../r2/shared/util/resource/Container.kt | 4 +- .../shared/util/resource/ContentResource.kt | 11 +-- .../util/resource/DirectoryContainer.kt | 2 +- .../util/resource/FileChannelResource.kt | 11 +-- .../r2/shared/util/resource/FileResource.kt | 8 +- .../util/resource/FileZipArchiveFactory.kt | 17 ++++- .../r2/shared/util/resource/Resource.kt | 76 ++++++++++++------- .../shared/util/resource/RoutingContainer.kt | 2 +- .../r2/shared/util/resource/ZipContainer.kt | 12 +-- .../content/ResourceContentExtractor.kt | 2 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 6 +- 18 files changed, 129 insertions(+), 90 deletions(-) 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 c59201e3e8..e17dc0cbb0 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 @@ -33,20 +33,18 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory = PsPdfKitDocument::class override suspend fun open(resource: Resource, password: String?): ResourceTry = - open(context, DocumentSource(ResourceDataProvider(resource), password)) - - private suspend fun open(context: Context, documentSource: DocumentSource): ResourceTry = withContext(Dispatchers.IO) { try { + val documentSource = DocumentSource(ResourceDataProvider(resource), password) Try.success( PsPdfKitDocument(PdfDocumentLoader.openDocument(context, documentSource)) ) } catch (e: InvalidPasswordException) { - Try.failure(Resource.Exception.Forbidden(e)) + Try.failure(Resource.Exception.Forbidden(resource, e)) } catch (e: CancellationException) { throw e } catch (e: Throwable) { - Try.failure(Resource.Exception.wrap(e)) + Try.failure(Resource.Exception.wrap(resource, e)) } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 0193a34a09..147b655eba 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -51,7 +51,7 @@ internal class LcpDecryptor( } when { - license == null -> FailureResource(Resource.Exception.Forbidden()) + license == null -> FailureResource(Resource.Exception.Forbidden(resource)) encryption.isDeflated || !encryption.isCbcEncrypted -> FullLcpResource( resource, encryption, @@ -76,7 +76,7 @@ internal class LcpDecryptor( ) : TransformingResource(resource) { override suspend fun transform(data: ResourceTry): ResourceTry = - license.decryptFully(data, encryption.isDeflated) + license.decryptFully(this, data, encryption.isDeflated) override suspend fun length(): ResourceTry = encryption.originalLength?.let { Try.success(it) } @@ -127,14 +127,14 @@ internal class LcpDecryptor( } private suspend fun lengthFromPadding(): ResourceTry = - resource.length().flatMapCatching { length -> + resource.length().flatMapCatching(resource) { length -> if (length < 2 * AES_BLOCK_SIZE) { throw Exception("Invalid CBC-encrypted stream") } val readOffset = length - (2 * AES_BLOCK_SIZE) resource.read(readOffset..length) - .mapCatching { bytes -> + .mapCatching(resource) { bytes -> val decryptedBytes = license.decrypt(bytes) .getOrElse { throw Exception( @@ -152,7 +152,7 @@ internal class LcpDecryptor( override suspend fun read(range: LongRange?): ResourceTry { if (range == null) { - return license.decryptFully(resource.read(), isDeflated = false) + return license.decryptFully(resource, resource.read(), isDeflated = false) } @Suppress("NAME_SHADOWING") @@ -164,13 +164,13 @@ internal class LcpDecryptor( return Try.success(ByteArray(0)) } - return resource.length().flatMapCatching { encryptedLength -> + return resource.length().flatMapCatching(resource) { encryptedLength -> // encrypted data is shifted by AES_BLOCK_SIZE because of IV and // the previous block must be provided to perform XOR on intermediate blocks val encryptedStart = range.first.floorMultipleOf(AES_BLOCK_SIZE.toLong()) val encryptedEndExclusive = (range.last + 1).ceilMultipleOf(AES_BLOCK_SIZE.toLong()) + AES_BLOCK_SIZE - getEncryptedData(encryptedStart until encryptedEndExclusive).mapCatching { encryptedData -> + getEncryptedData(encryptedStart until encryptedEndExclusive).mapCatching(resource) { encryptedData -> if (encryptedData.size >= _cache.data.size) { // cache the three last encrypted blocks that have been read for future use val cacheStart = encryptedData.size - _cache.data.size @@ -233,8 +233,12 @@ internal class LcpDecryptor( } } -private suspend fun LcpLicense.decryptFully(data: ResourceTry, isDeflated: Boolean): ResourceTry = - data.mapCatching { encryptedData -> +private suspend fun LcpLicense.decryptFully( + resource: Resource, + data: ResourceTry, + isDeflated: Boolean +): ResourceTry = + data.mapCatching(resource) { encryptedData -> // Decrypts the resource. var bytes = decrypt(encryptedData) .getOrElse { throw Exception("Failed to decrypt the resource", it) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index 28da6ca47e..2209654b92 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -122,7 +122,7 @@ internal suspend fun Resource.readByChunks( groundTruth: ByteArray, shuffle: Boolean = true ) = - length().mapCatching { length -> + length().mapCatching(this) { length -> val blockNb = ceil(length / chunkSize.toDouble()).toInt() val blocks = (0 until blockNb) .map { Pair(it, it * chunkSize until kotlin.math.min(length, (it + 1) * chunkSize)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index e2e0d527b3..ee89d6231b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -297,12 +297,14 @@ private sealed class RouteHandler { val text = query.firstNamedOrNull("text") ?: return FailureResource( Resource.Exception.BadRequest( + url = url, IllegalArgumentException("'text' parameter is required") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( Resource.Exception.BadRequest( + url = url, IllegalArgumentException("if present, 'peek' must be true or false") ) ) @@ -310,7 +312,7 @@ private sealed class RouteHandler { val copyAllowed = with(service.rights) { if (peek) canCopy(text) else copy(text) } return if (!copyAllowed) { - FailureResource(Resource.Exception.Forbidden()) + FailureResource(Resource.Exception.Forbidden(url)) } else { StringResource("true", MediaType.JSON) } @@ -335,6 +337,7 @@ private sealed class RouteHandler { val pageCountString = query.firstNamedOrNull("pageCount") ?: return FailureResource( Resource.Exception.BadRequest( + url = url, IllegalArgumentException("'pageCount' parameter is required") ) ) @@ -342,12 +345,14 @@ private sealed class RouteHandler { val pageCount = pageCountString.toIntOrNull()?.takeIf { it >= 0 } ?: return FailureResource( Resource.Exception.BadRequest( + url = url, IllegalArgumentException("'pageCount' must be a positive integer") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( Resource.Exception.BadRequest( + url = url, IllegalArgumentException("if present, 'peek' must be true or false") ) ) @@ -363,7 +368,7 @@ private sealed class RouteHandler { } return if (!printAllowed) { - FailureResource(Resource.Exception.Forbidden()) + FailureResource(Resource.Exception.Forbidden(url)) } else { StringResource("true", mediaType = MediaType.JSON) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index e530bf5915..91918e8ee9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -111,7 +111,7 @@ public abstract class GeneratedCoverService : CoverService { if (png == null) { val error = Exception("Unable to convert cover to PNG.") - FailureResource(error) + FailureResource(href, error) } else { BytesResource(png, mediaType = MediaType.PNG) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index 10f7e1d62e..dc49c23633 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -35,6 +35,7 @@ public class HttpContainer( return if (absoluteUrl == null || !absoluteUrl.isHttp) { FailureResource( Resource.Exception.NotFound( + url, Exception("URL scheme is not supported: ${absoluteUrl?.scheme}.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index 6ccc93be4d..c7c9a6bb4a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -34,7 +34,7 @@ public class HttpResource( return if (contentLength != null) { Try.success(contentLength) } else { - Try.failure(Resource.Exception.Unavailable()) + Try.failure(Resource.Exception.Unavailable(source)) } } @@ -52,9 +52,9 @@ public class HttpResource( } } } catch (e: HttpException) { - Try.failure(Resource.Exception.wrapHttp(e)) + Try.failure(Resource.Exception.wrapHttp(source, e)) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) + Try.failure(Resource.Exception.wrap(source, e)) } } @@ -67,7 +67,7 @@ public class HttpResource( } _headResponse = client.head(HttpRequest(source.toString())) - .mapFailure { Resource.Exception.wrapHttp(it) } + .mapFailure { Resource.Exception.wrapHttp(source, it) } return _headResponse } @@ -106,7 +106,7 @@ public class HttpResource( } } .map { CountingInputStream(it.body) } - .mapFailure { Resource.Exception.wrapHttp(it) } + .mapFailure { Resource.Exception.wrapHttp(source, it) } .onSuccess { inputStream = it inputStreamStart = from ?: 0 @@ -116,20 +116,20 @@ public class HttpResource( private var inputStream: CountingInputStream? = null private var inputStreamStart = 0L - private fun Resource.Exception.Companion.wrapHttp(e: HttpException): Resource.Exception = + private fun Resource.Exception.Companion.wrapHttp(url: AbsoluteUrl, e: HttpException): Resource.Exception = when (e.kind) { HttpException.Kind.MalformedRequest, HttpException.Kind.BadRequest, HttpException.Kind.MethodNotAllowed -> - Resource.Exception.BadRequest(cause = e) + Resource.Exception.BadRequest(url, cause = e) HttpException.Kind.Timeout, HttpException.Kind.Offline -> - Resource.Exception.Unavailable(e) + Resource.Exception.Unavailable(url, e) HttpException.Kind.Unauthorized, HttpException.Kind.Forbidden -> - Resource.Exception.Forbidden(e) + Resource.Exception.Forbidden(url, e) HttpException.Kind.NotFound -> - Resource.Exception.NotFound(e) + Resource.Exception.NotFound(url, e) HttpException.Kind.Cancelled -> - Resource.Exception.Unavailable(e) + Resource.Exception.Unavailable(url, e) HttpException.Kind.MalformedResponse, HttpException.Kind.ClientError, HttpException.Kind.ServerError, HttpException.Kind.Other -> - Resource.Exception.Other(e) + Resource.Exception.Other(url, e) } public companion object { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt index db476f78ef..79f825ed45 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt @@ -51,7 +51,7 @@ public class EmptyContainer : Container { override suspend fun entries(): Set = emptySet() override fun get(url: Url): Container.Entry = - FailureResource(Resource.Exception.NotFound()).toEntry(url) + FailureResource(Resource.Exception.NotFound(url)).toEntry(url) override suspend fun close() {} } @@ -65,7 +65,7 @@ public class ResourceContainer(url: Url, resource: Resource) : Container { override fun get(url: Url): Container.Entry { if (url.removeFragment().removeQuery() != entry.url) { - return FailureResource(Resource.Exception.NotFound()).toEntry(url) + return FailureResource(Resource.Exception.NotFound(url)).toEntry(url) } return entry diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index bc40f9dc81..cb8f1d78fd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -105,6 +105,7 @@ public class ContentResource( ResourceTry.catching { val stream = contentResolver.openInputStream(uri) ?: throw Resource.Exception.Unavailable( + source, Exception("Content provider recently crashed.") ) val result = block(stream) @@ -116,15 +117,15 @@ public class ContentResource( try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(e)) + failure(Resource.Exception.NotFound(source, e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(e)) + failure(Resource.Exception.Forbidden(source, e)) } catch (e: IOException) { - failure(Resource.Exception.Unavailable(e)) + failure(Resource.Exception.Unavailable(source, e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(e)) + failure(Resource.Exception.wrap(source, e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(e)) + failure(Resource.Exception.wrap(source, e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 11212b834e..98966a39cd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -43,7 +43,7 @@ internal class DirectoryContainer( ?.let { File(root, it) } return if (file == null || !root.isParentOf(file)) { - FailureResource(Resource.Exception.NotFound()).toEntry(url) + FailureResource(Resource.Exception.NotFound(url)).toEntry(url) } else { FileEntry(url, file) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt index 57cba9ef76..1ca923a697 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt @@ -17,6 +17,7 @@ import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.toUrl internal class FileChannelResource( override val source: AbsoluteUrl?, @@ -82,7 +83,7 @@ internal class FileChannelResource( check(channel.isOpen) Try.success(channel.size()) } catch (e: IOException) { - Try.failure(Resource.Exception.Unavailable(e)) + Try.failure(Resource.Exception.Unavailable(file?.toUrl(), e)) } } } @@ -94,13 +95,13 @@ internal class FileChannelResource( try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(e)) + failure(Resource.Exception.NotFound(file?.toUrl(), e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(e)) + failure(Resource.Exception.Forbidden(file?.toUrl(), e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(e)) + failure(Resource.Exception.wrap(file?.toUrl(), e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(e)) + failure(Resource.Exception.wrap(file?.toUrl(), e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt index d190ee4762..868571bbcd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt @@ -119,13 +119,13 @@ public class FileResource private constructor( try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(e)) + failure(Resource.Exception.NotFound(file.toUrl(), e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(e)) + failure(Resource.Exception.Forbidden(file.toUrl(), e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(e)) + failure(Resource.Exception.wrap(file.toUrl(), e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(e)) + failure(Resource.Exception.wrap(file.toUrl(), e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt index 3c100f57fa..3aa3edfe76 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.toUrl /** * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. @@ -31,7 +32,9 @@ public class FileZipArchiveFactory( ?.let { open(it) } ?: Try.Failure( ArchiveFactory.Error.FormatNotSupported( - MessageError("Resource not supported because file cannot be directly accessed.") + MessageError( + "Resource not supported because the file cannot be directly accessed." + ) ) ) } @@ -45,9 +48,17 @@ public class FileZipArchiveFactory( } catch (e: ZipException) { Try.failure(ArchiveFactory.Error.FormatNotSupported(e)) } catch (e: SecurityException) { - Try.failure(ArchiveFactory.Error.ResourceReading(Resource.Exception.Forbidden(e))) + Try.failure( + ArchiveFactory.Error.ResourceReading( + Resource.Exception.Forbidden(file.toUrl(), e) + ) + ) } catch (e: Exception) { - Try.failure(ArchiveFactory.Error.ResourceReading(Resource.Exception.wrap(e))) + Try.failure( + ArchiveFactory.Error.ResourceReading( + Resource.Exception.wrap(file.toUrl(), e) + ) + ) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index bd65824f02..159b093935 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -17,6 +17,7 @@ import org.readium.r2.shared.UserException import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.xml.ElementNode @@ -80,19 +81,25 @@ public interface Resource : SuspendingCloseable { /** * Errors occurring while accessing a resource. + * + * @param url URL locating the resource, if any. */ - public sealed class Exception(@StringRes userMessageId: Int, cause: Throwable? = null) : UserException( + public sealed class Exception( + public val url: Url?, + @StringRes userMessageId: Int, + cause: Throwable? = null + ) : UserException( userMessageId, cause = cause ) { /** Equivalent to a 400 HTTP error. */ - public class BadRequest(cause: Throwable? = null) : - Exception(R.string.readium_shared_resource_exception_bad_request, cause) + public class BadRequest(url: Url?, cause: Throwable? = null) : + Exception(url, R.string.readium_shared_resource_exception_bad_request, cause) /** Equivalent to a 404 HTTP error. */ - public class NotFound(cause: Throwable? = null) : - Exception(R.string.readium_shared_resource_exception_not_found, cause) + public class NotFound(url: Url?, cause: Throwable? = null) : + Exception(url, R.string.readium_shared_resource_exception_not_found, cause) /** * Equivalent to a 403 HTTP error. @@ -100,8 +107,11 @@ public interface Resource : SuspendingCloseable { * This can be returned when trying to read a resource protected with a DRM that is not * unlocked. */ - public class Forbidden(cause: Throwable? = null) : - Exception(R.string.readium_shared_resource_exception_forbidden, cause) + public class Forbidden(url: Url?, cause: Throwable? = null) : + Exception(url, R.string.readium_shared_resource_exception_forbidden, cause) { + public constructor(resource: Resource, cause: Throwable? = null) : + this(resource.url, cause) + } /** * Equivalent to a 503 HTTP error. @@ -109,46 +119,51 @@ public interface Resource : SuspendingCloseable { * Used when the source can't be reached, e.g. no Internet connection, or an issue with the * file system. Usually this is a temporary error. */ - public class Unavailable(cause: Throwable? = null) : - Exception(R.string.readium_shared_resource_exception_unavailable, cause) + public class Unavailable(url: Url?, cause: Throwable? = null) : + Exception(url, R.string.readium_shared_resource_exception_unavailable, cause) /** * The Internet connection appears to be offline. */ - public object Offline : Exception(R.string.readium_shared_resource_exception_offline) + public data object Offline : + Exception(null, R.string.readium_shared_resource_exception_offline) /** * Equivalent to a 507 HTTP error. * * Used when the requested range is too large to be read in memory. */ - public class OutOfMemory(override val cause: OutOfMemoryError) : - Exception(R.string.readium_shared_resource_exception_out_of_memory) + public class OutOfMemory(url: Url?, override val cause: OutOfMemoryError) : + Exception(url, R.string.readium_shared_resource_exception_out_of_memory) /** For any other error, such as HTTP 500. */ - public class Other(cause: Throwable) : Exception( - R.string.readium_shared_resource_exception_other, - cause - ) + public class Other(url: Url?, cause: Throwable) : + Exception(url, R.string.readium_shared_resource_exception_other, cause) public companion object { - public fun wrap(e: Throwable): Exception = + public fun wrap(resource: Resource?, e: Throwable): Exception = + wrap(resource?.url, e) + + public fun wrap(url: Url?, e: Throwable): Exception = when (e) { is Exception -> e - is OutOfMemoryError -> OutOfMemory(e) - else -> Other(e) + is OutOfMemoryError -> OutOfMemory(url, e) + else -> Other(url, e) } } } } +private val Resource.url: Url? + get() = source ?: (this as? Container.Entry)?.url + /** Creates a Resource that will always return the given [error]. */ public class FailureResource( private val error: Resource.Exception ) : Resource { - internal constructor(cause: Throwable) : this(Resource.Exception.wrap(cause)) + internal constructor(url: Url?, cause: Throwable) : this(Resource.Exception.wrap(url, cause)) override val source: AbsoluteUrl? = null override suspend fun mediaType(): ResourceTry = Try.failure(error) @@ -166,17 +181,20 @@ public class FailureResource( * * If the [transform] throws an [Exception], it is wrapped in a failure with Resource.Exception.Other. */ -public inline fun ResourceTry.mapCatching(transform: (value: S) -> R): ResourceTry = +public inline fun ResourceTry.mapCatching(resource: Resource, transform: (value: S) -> R): ResourceTry = try { map(transform) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) + Try.failure(Resource.Exception.wrap(resource, e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Try.failure(Resource.Exception.wrap(e)) + Try.failure(Resource.Exception.wrap(resource, e)) } -public inline fun ResourceTry.flatMapCatching(transform: (value: S) -> ResourceTry): ResourceTry = - mapCatching(transform).flatMap { it } +public inline fun ResourceTry.flatMapCatching( + resource: Resource, + transform: (value: S) -> ResourceTry +): ResourceTry = + mapCatching(resource, transform).flatMap { it } /** * Reads the full content as a [String]. @@ -185,7 +203,7 @@ public inline fun ResourceTry.flatMapCatching(transform: (value: S) -> * or falls back on UTF-8. */ public suspend fun Resource.readAsString(charset: Charset? = null): ResourceTry = - read().mapCatching { + read().mapCatching(this) { String(it, charset = charset ?: Charsets.UTF_8) } @@ -193,19 +211,19 @@ public suspend fun Resource.readAsString(charset: Charset? = null): ResourceTry< * Reads the full content as a JSON object. */ public suspend fun Resource.readAsJson(): ResourceTry = - readAsString(charset = Charsets.UTF_8).mapCatching { JSONObject(it) } + readAsString(charset = Charsets.UTF_8).mapCatching(this) { JSONObject(it) } /** * Reads the full content as an XML document. */ public suspend fun Resource.readAsXml(): ResourceTry = - read().mapCatching { XmlParser().parse(ByteArrayInputStream(it)) } + read().mapCatching(this) { XmlParser().parse(ByteArrayInputStream(it)) } /** * Reads the full content as a [Bitmap]. */ public suspend fun Resource.readAsBitmap(): ResourceTry = - read().mapCatching { + read().mapCatching(this) { BitmapFactory.decodeByteArray(it, 0, it.size) ?: throw kotlin.Exception("Could not decode resource as a bitmap") } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt index a7fa47e118..a228b5858e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt @@ -42,7 +42,7 @@ public class RoutingContainer(private val routes: List) : Container { override fun get(url: Url): Container.Entry = routes.firstOrNull { it.accepts(url) }?.container?.get(url) - ?: FailureResource(Resource.Exception.NotFound()).toEntry(url) + ?: FailureResource(Resource.Exception.NotFound(url)).toEntry(url) override suspend fun close() { routes.forEach { it.container.close() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt index 2b8bd22bdd..26f1769c8b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt @@ -101,13 +101,13 @@ internal class JavaZipContainer( ) override suspend fun properties(): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(Resource.Exception.NotFound(url)) override suspend fun length(): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(Resource.Exception.NotFound(url)) override suspend fun read(range: LongRange?): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(Resource.Exception.NotFound(url)) override suspend fun close() { } @@ -139,7 +139,7 @@ internal class JavaZipContainer( override suspend fun length(): Try = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(Resource.Exception.Other(Exception("Unsupported operation"))) + ?: Try.failure(Resource.Exception.Other(url, Exception("Unsupported operation"))) private val compressedLength: Long? = if (entry.method == ZipEntry.STORED || entry.method == -1) { @@ -160,9 +160,9 @@ internal class JavaZipContainer( Try.success(bytes) } } catch (e: IOException) { - Try.failure(Resource.Exception.Unavailable(e)) + Try.failure(Resource.Exception.Unavailable(url, e)) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) + Try.failure(Resource.Exception.wrap(url, e)) } private suspend fun readFully(): ByteArray = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index 8de612337e..07fa308357 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -58,7 +58,7 @@ public class HtmlResourceContentExtractor : ResourceContentExtractor { override suspend fun extractText(resource: Resource): ResourceTry = withContext( Dispatchers.IO ) { - resource.readAsString().mapCatching { html -> + resource.readAsString().mapCatching(resource) { html -> val body = Jsoup.parse(html).body().text() // Transform HTML entities into their actual characters. Parser.unescapeEntities(body, false) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index 80eecf39be..706040cc72 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -37,7 +37,7 @@ internal class ChannelZipContainer( private inner class FailureEntry( override val url: Url - ) : Container.Entry, Resource by FailureResource(Resource.Exception.NotFound()) + ) : Container.Entry, Resource by FailureResource(Resource.Exception.NotFound(url)) private inner class Entry( override val url: Url, @@ -68,7 +68,7 @@ internal class ChannelZipContainer( override suspend fun length(): ResourceTry = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(Resource.Exception.Other(UnsupportedOperationException())) + ?: Try.failure(Resource.Exception.Other(url, UnsupportedOperationException())) private val compressedLength: Long? get() = @@ -89,7 +89,7 @@ internal class ChannelZipContainer( } Try.success(bytes) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) + Try.failure(Resource.Exception.wrap(url, e)) } } From 457a0327bee312dabe2c28315d752740b59a3da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Thu, 5 Oct 2023 09:47:51 +0200 Subject: [PATCH 04/11] Revert "Add the `url` in `Resource.Exception`" This reverts commit ca59dea9bc0fda8c20383f28b44ed92c2860e0de. --- .../pspdfkit/document/PsPdfKitDocument.kt | 8 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 22 +++--- .../org/readium/r2/lcp/LcpDecryptorTest.kt | 2 +- .../services/ContentProtectionService.kt | 9 +-- .../publication/services/CoverService.kt | 2 +- .../r2/shared/util/http/HttpContainer.kt | 1 - .../r2/shared/util/http/HttpResource.kt | 24 +++--- .../r2/shared/util/resource/Container.kt | 4 +- .../shared/util/resource/ContentResource.kt | 11 ++- .../util/resource/DirectoryContainer.kt | 2 +- .../util/resource/FileChannelResource.kt | 11 ++- .../r2/shared/util/resource/FileResource.kt | 8 +- .../util/resource/FileZipArchiveFactory.kt | 17 +---- .../r2/shared/util/resource/Resource.kt | 76 +++++++------------ .../shared/util/resource/RoutingContainer.kt | 2 +- .../r2/shared/util/resource/ZipContainer.kt | 12 +-- .../content/ResourceContentExtractor.kt | 2 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 6 +- 18 files changed, 90 insertions(+), 129 deletions(-) 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 e17dc0cbb0..c59201e3e8 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 @@ -33,18 +33,20 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory = PsPdfKitDocument::class override suspend fun open(resource: Resource, password: String?): ResourceTry = + open(context, DocumentSource(ResourceDataProvider(resource), password)) + + private suspend fun open(context: Context, documentSource: DocumentSource): ResourceTry = withContext(Dispatchers.IO) { try { - val documentSource = DocumentSource(ResourceDataProvider(resource), password) Try.success( PsPdfKitDocument(PdfDocumentLoader.openDocument(context, documentSource)) ) } catch (e: InvalidPasswordException) { - Try.failure(Resource.Exception.Forbidden(resource, e)) + Try.failure(Resource.Exception.Forbidden(e)) } catch (e: CancellationException) { throw e } catch (e: Throwable) { - Try.failure(Resource.Exception.wrap(resource, e)) + Try.failure(Resource.Exception.wrap(e)) } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 147b655eba..0193a34a09 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -51,7 +51,7 @@ internal class LcpDecryptor( } when { - license == null -> FailureResource(Resource.Exception.Forbidden(resource)) + license == null -> FailureResource(Resource.Exception.Forbidden()) encryption.isDeflated || !encryption.isCbcEncrypted -> FullLcpResource( resource, encryption, @@ -76,7 +76,7 @@ internal class LcpDecryptor( ) : TransformingResource(resource) { override suspend fun transform(data: ResourceTry): ResourceTry = - license.decryptFully(this, data, encryption.isDeflated) + license.decryptFully(data, encryption.isDeflated) override suspend fun length(): ResourceTry = encryption.originalLength?.let { Try.success(it) } @@ -127,14 +127,14 @@ internal class LcpDecryptor( } private suspend fun lengthFromPadding(): ResourceTry = - resource.length().flatMapCatching(resource) { length -> + resource.length().flatMapCatching { length -> if (length < 2 * AES_BLOCK_SIZE) { throw Exception("Invalid CBC-encrypted stream") } val readOffset = length - (2 * AES_BLOCK_SIZE) resource.read(readOffset..length) - .mapCatching(resource) { bytes -> + .mapCatching { bytes -> val decryptedBytes = license.decrypt(bytes) .getOrElse { throw Exception( @@ -152,7 +152,7 @@ internal class LcpDecryptor( override suspend fun read(range: LongRange?): ResourceTry { if (range == null) { - return license.decryptFully(resource, resource.read(), isDeflated = false) + return license.decryptFully(resource.read(), isDeflated = false) } @Suppress("NAME_SHADOWING") @@ -164,13 +164,13 @@ internal class LcpDecryptor( return Try.success(ByteArray(0)) } - return resource.length().flatMapCatching(resource) { encryptedLength -> + return resource.length().flatMapCatching { encryptedLength -> // encrypted data is shifted by AES_BLOCK_SIZE because of IV and // the previous block must be provided to perform XOR on intermediate blocks val encryptedStart = range.first.floorMultipleOf(AES_BLOCK_SIZE.toLong()) val encryptedEndExclusive = (range.last + 1).ceilMultipleOf(AES_BLOCK_SIZE.toLong()) + AES_BLOCK_SIZE - getEncryptedData(encryptedStart until encryptedEndExclusive).mapCatching(resource) { encryptedData -> + getEncryptedData(encryptedStart until encryptedEndExclusive).mapCatching { encryptedData -> if (encryptedData.size >= _cache.data.size) { // cache the three last encrypted blocks that have been read for future use val cacheStart = encryptedData.size - _cache.data.size @@ -233,12 +233,8 @@ internal class LcpDecryptor( } } -private suspend fun LcpLicense.decryptFully( - resource: Resource, - data: ResourceTry, - isDeflated: Boolean -): ResourceTry = - data.mapCatching(resource) { encryptedData -> +private suspend fun LcpLicense.decryptFully(data: ResourceTry, isDeflated: Boolean): ResourceTry = + data.mapCatching { encryptedData -> // Decrypts the resource. var bytes = decrypt(encryptedData) .getOrElse { throw Exception("Failed to decrypt the resource", it) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index 2209654b92..28da6ca47e 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -122,7 +122,7 @@ internal suspend fun Resource.readByChunks( groundTruth: ByteArray, shuffle: Boolean = true ) = - length().mapCatching(this) { length -> + length().mapCatching { length -> val blockNb = ceil(length / chunkSize.toDouble()).toInt() val blocks = (0 until blockNb) .map { Pair(it, it * chunkSize until kotlin.math.min(length, (it + 1) * chunkSize)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index ee89d6231b..e2e0d527b3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -297,14 +297,12 @@ private sealed class RouteHandler { val text = query.firstNamedOrNull("text") ?: return FailureResource( Resource.Exception.BadRequest( - url = url, IllegalArgumentException("'text' parameter is required") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( Resource.Exception.BadRequest( - url = url, IllegalArgumentException("if present, 'peek' must be true or false") ) ) @@ -312,7 +310,7 @@ private sealed class RouteHandler { val copyAllowed = with(service.rights) { if (peek) canCopy(text) else copy(text) } return if (!copyAllowed) { - FailureResource(Resource.Exception.Forbidden(url)) + FailureResource(Resource.Exception.Forbidden()) } else { StringResource("true", MediaType.JSON) } @@ -337,7 +335,6 @@ private sealed class RouteHandler { val pageCountString = query.firstNamedOrNull("pageCount") ?: return FailureResource( Resource.Exception.BadRequest( - url = url, IllegalArgumentException("'pageCount' parameter is required") ) ) @@ -345,14 +342,12 @@ private sealed class RouteHandler { val pageCount = pageCountString.toIntOrNull()?.takeIf { it >= 0 } ?: return FailureResource( Resource.Exception.BadRequest( - url = url, IllegalArgumentException("'pageCount' must be a positive integer") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( Resource.Exception.BadRequest( - url = url, IllegalArgumentException("if present, 'peek' must be true or false") ) ) @@ -368,7 +363,7 @@ private sealed class RouteHandler { } return if (!printAllowed) { - FailureResource(Resource.Exception.Forbidden(url)) + FailureResource(Resource.Exception.Forbidden()) } else { StringResource("true", mediaType = MediaType.JSON) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index 91918e8ee9..e530bf5915 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -111,7 +111,7 @@ public abstract class GeneratedCoverService : CoverService { if (png == null) { val error = Exception("Unable to convert cover to PNG.") - FailureResource(href, error) + FailureResource(error) } else { BytesResource(png, mediaType = MediaType.PNG) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index dc49c23633..10f7e1d62e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -35,7 +35,6 @@ public class HttpContainer( return if (absoluteUrl == null || !absoluteUrl.isHttp) { FailureResource( Resource.Exception.NotFound( - url, Exception("URL scheme is not supported: ${absoluteUrl?.scheme}.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index c7c9a6bb4a..6ccc93be4d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -34,7 +34,7 @@ public class HttpResource( return if (contentLength != null) { Try.success(contentLength) } else { - Try.failure(Resource.Exception.Unavailable(source)) + Try.failure(Resource.Exception.Unavailable()) } } @@ -52,9 +52,9 @@ public class HttpResource( } } } catch (e: HttpException) { - Try.failure(Resource.Exception.wrapHttp(source, e)) + Try.failure(Resource.Exception.wrapHttp(e)) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(source, e)) + Try.failure(Resource.Exception.wrap(e)) } } @@ -67,7 +67,7 @@ public class HttpResource( } _headResponse = client.head(HttpRequest(source.toString())) - .mapFailure { Resource.Exception.wrapHttp(source, it) } + .mapFailure { Resource.Exception.wrapHttp(it) } return _headResponse } @@ -106,7 +106,7 @@ public class HttpResource( } } .map { CountingInputStream(it.body) } - .mapFailure { Resource.Exception.wrapHttp(source, it) } + .mapFailure { Resource.Exception.wrapHttp(it) } .onSuccess { inputStream = it inputStreamStart = from ?: 0 @@ -116,20 +116,20 @@ public class HttpResource( private var inputStream: CountingInputStream? = null private var inputStreamStart = 0L - private fun Resource.Exception.Companion.wrapHttp(url: AbsoluteUrl, e: HttpException): Resource.Exception = + private fun Resource.Exception.Companion.wrapHttp(e: HttpException): Resource.Exception = when (e.kind) { HttpException.Kind.MalformedRequest, HttpException.Kind.BadRequest, HttpException.Kind.MethodNotAllowed -> - Resource.Exception.BadRequest(url, cause = e) + Resource.Exception.BadRequest(cause = e) HttpException.Kind.Timeout, HttpException.Kind.Offline -> - Resource.Exception.Unavailable(url, e) + Resource.Exception.Unavailable(e) HttpException.Kind.Unauthorized, HttpException.Kind.Forbidden -> - Resource.Exception.Forbidden(url, e) + Resource.Exception.Forbidden(e) HttpException.Kind.NotFound -> - Resource.Exception.NotFound(url, e) + Resource.Exception.NotFound(e) HttpException.Kind.Cancelled -> - Resource.Exception.Unavailable(url, e) + Resource.Exception.Unavailable(e) HttpException.Kind.MalformedResponse, HttpException.Kind.ClientError, HttpException.Kind.ServerError, HttpException.Kind.Other -> - Resource.Exception.Other(url, e) + Resource.Exception.Other(e) } public companion object { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt index 79f825ed45..db476f78ef 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt @@ -51,7 +51,7 @@ public class EmptyContainer : Container { override suspend fun entries(): Set = emptySet() override fun get(url: Url): Container.Entry = - FailureResource(Resource.Exception.NotFound(url)).toEntry(url) + FailureResource(Resource.Exception.NotFound()).toEntry(url) override suspend fun close() {} } @@ -65,7 +65,7 @@ public class ResourceContainer(url: Url, resource: Resource) : Container { override fun get(url: Url): Container.Entry { if (url.removeFragment().removeQuery() != entry.url) { - return FailureResource(Resource.Exception.NotFound(url)).toEntry(url) + return FailureResource(Resource.Exception.NotFound()).toEntry(url) } return entry diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index cb8f1d78fd..bc40f9dc81 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -105,7 +105,6 @@ public class ContentResource( ResourceTry.catching { val stream = contentResolver.openInputStream(uri) ?: throw Resource.Exception.Unavailable( - source, Exception("Content provider recently crashed.") ) val result = block(stream) @@ -117,15 +116,15 @@ public class ContentResource( try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(source, e)) + failure(Resource.Exception.NotFound(e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(source, e)) + failure(Resource.Exception.Forbidden(e)) } catch (e: IOException) { - failure(Resource.Exception.Unavailable(source, e)) + failure(Resource.Exception.Unavailable(e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(source, e)) + failure(Resource.Exception.wrap(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(source, e)) + failure(Resource.Exception.wrap(e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 98966a39cd..11212b834e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -43,7 +43,7 @@ internal class DirectoryContainer( ?.let { File(root, it) } return if (file == null || !root.isParentOf(file)) { - FailureResource(Resource.Exception.NotFound(url)).toEntry(url) + FailureResource(Resource.Exception.NotFound()).toEntry(url) } else { FileEntry(url, file) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt index 1ca923a697..57cba9ef76 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt @@ -17,7 +17,6 @@ import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.toUrl internal class FileChannelResource( override val source: AbsoluteUrl?, @@ -83,7 +82,7 @@ internal class FileChannelResource( check(channel.isOpen) Try.success(channel.size()) } catch (e: IOException) { - Try.failure(Resource.Exception.Unavailable(file?.toUrl(), e)) + Try.failure(Resource.Exception.Unavailable(e)) } } } @@ -95,13 +94,13 @@ internal class FileChannelResource( try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(file?.toUrl(), e)) + failure(Resource.Exception.NotFound(e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(file?.toUrl(), e)) + failure(Resource.Exception.Forbidden(e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(file?.toUrl(), e)) + failure(Resource.Exception.wrap(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(file?.toUrl(), e)) + failure(Resource.Exception.wrap(e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt index 868571bbcd..d190ee4762 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt @@ -119,13 +119,13 @@ public class FileResource private constructor( try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(file.toUrl(), e)) + failure(Resource.Exception.NotFound(e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(file.toUrl(), e)) + failure(Resource.Exception.Forbidden(e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(file.toUrl(), e)) + failure(Resource.Exception.wrap(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(file.toUrl(), e)) + failure(Resource.Exception.wrap(e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt index 3aa3edfe76..3c100f57fa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.toUrl /** * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. @@ -32,9 +31,7 @@ public class FileZipArchiveFactory( ?.let { open(it) } ?: Try.Failure( ArchiveFactory.Error.FormatNotSupported( - MessageError( - "Resource not supported because the file cannot be directly accessed." - ) + MessageError("Resource not supported because file cannot be directly accessed.") ) ) } @@ -48,17 +45,9 @@ public class FileZipArchiveFactory( } catch (e: ZipException) { Try.failure(ArchiveFactory.Error.FormatNotSupported(e)) } catch (e: SecurityException) { - Try.failure( - ArchiveFactory.Error.ResourceReading( - Resource.Exception.Forbidden(file.toUrl(), e) - ) - ) + Try.failure(ArchiveFactory.Error.ResourceReading(Resource.Exception.Forbidden(e))) } catch (e: Exception) { - Try.failure( - ArchiveFactory.Error.ResourceReading( - Resource.Exception.wrap(file.toUrl(), e) - ) - ) + Try.failure(ArchiveFactory.Error.ResourceReading(Resource.Exception.wrap(e))) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index 159b093935..bd65824f02 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -17,7 +17,6 @@ import org.readium.r2.shared.UserException import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.xml.ElementNode @@ -81,25 +80,19 @@ public interface Resource : SuspendingCloseable { /** * Errors occurring while accessing a resource. - * - * @param url URL locating the resource, if any. */ - public sealed class Exception( - public val url: Url?, - @StringRes userMessageId: Int, - cause: Throwable? = null - ) : UserException( + public sealed class Exception(@StringRes userMessageId: Int, cause: Throwable? = null) : UserException( userMessageId, cause = cause ) { /** Equivalent to a 400 HTTP error. */ - public class BadRequest(url: Url?, cause: Throwable? = null) : - Exception(url, R.string.readium_shared_resource_exception_bad_request, cause) + public class BadRequest(cause: Throwable? = null) : + Exception(R.string.readium_shared_resource_exception_bad_request, cause) /** Equivalent to a 404 HTTP error. */ - public class NotFound(url: Url?, cause: Throwable? = null) : - Exception(url, R.string.readium_shared_resource_exception_not_found, cause) + public class NotFound(cause: Throwable? = null) : + Exception(R.string.readium_shared_resource_exception_not_found, cause) /** * Equivalent to a 403 HTTP error. @@ -107,11 +100,8 @@ public interface Resource : SuspendingCloseable { * This can be returned when trying to read a resource protected with a DRM that is not * unlocked. */ - public class Forbidden(url: Url?, cause: Throwable? = null) : - Exception(url, R.string.readium_shared_resource_exception_forbidden, cause) { - public constructor(resource: Resource, cause: Throwable? = null) : - this(resource.url, cause) - } + public class Forbidden(cause: Throwable? = null) : + Exception(R.string.readium_shared_resource_exception_forbidden, cause) /** * Equivalent to a 503 HTTP error. @@ -119,51 +109,46 @@ public interface Resource : SuspendingCloseable { * Used when the source can't be reached, e.g. no Internet connection, or an issue with the * file system. Usually this is a temporary error. */ - public class Unavailable(url: Url?, cause: Throwable? = null) : - Exception(url, R.string.readium_shared_resource_exception_unavailable, cause) + public class Unavailable(cause: Throwable? = null) : + Exception(R.string.readium_shared_resource_exception_unavailable, cause) /** * The Internet connection appears to be offline. */ - public data object Offline : - Exception(null, R.string.readium_shared_resource_exception_offline) + public object Offline : Exception(R.string.readium_shared_resource_exception_offline) /** * Equivalent to a 507 HTTP error. * * Used when the requested range is too large to be read in memory. */ - public class OutOfMemory(url: Url?, override val cause: OutOfMemoryError) : - Exception(url, R.string.readium_shared_resource_exception_out_of_memory) + public class OutOfMemory(override val cause: OutOfMemoryError) : + Exception(R.string.readium_shared_resource_exception_out_of_memory) /** For any other error, such as HTTP 500. */ - public class Other(url: Url?, cause: Throwable) : - Exception(url, R.string.readium_shared_resource_exception_other, cause) + public class Other(cause: Throwable) : Exception( + R.string.readium_shared_resource_exception_other, + cause + ) public companion object { - public fun wrap(resource: Resource?, e: Throwable): Exception = - wrap(resource?.url, e) - - public fun wrap(url: Url?, e: Throwable): Exception = + public fun wrap(e: Throwable): Exception = when (e) { is Exception -> e - is OutOfMemoryError -> OutOfMemory(url, e) - else -> Other(url, e) + is OutOfMemoryError -> OutOfMemory(e) + else -> Other(e) } } } } -private val Resource.url: Url? - get() = source ?: (this as? Container.Entry)?.url - /** Creates a Resource that will always return the given [error]. */ public class FailureResource( private val error: Resource.Exception ) : Resource { - internal constructor(url: Url?, cause: Throwable) : this(Resource.Exception.wrap(url, cause)) + internal constructor(cause: Throwable) : this(Resource.Exception.wrap(cause)) override val source: AbsoluteUrl? = null override suspend fun mediaType(): ResourceTry = Try.failure(error) @@ -181,20 +166,17 @@ public class FailureResource( * * If the [transform] throws an [Exception], it is wrapped in a failure with Resource.Exception.Other. */ -public inline fun ResourceTry.mapCatching(resource: Resource, transform: (value: S) -> R): ResourceTry = +public inline fun ResourceTry.mapCatching(transform: (value: S) -> R): ResourceTry = try { map(transform) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(resource, e)) + Try.failure(Resource.Exception.wrap(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Try.failure(Resource.Exception.wrap(resource, e)) + Try.failure(Resource.Exception.wrap(e)) } -public inline fun ResourceTry.flatMapCatching( - resource: Resource, - transform: (value: S) -> ResourceTry -): ResourceTry = - mapCatching(resource, transform).flatMap { it } +public inline fun ResourceTry.flatMapCatching(transform: (value: S) -> ResourceTry): ResourceTry = + mapCatching(transform).flatMap { it } /** * Reads the full content as a [String]. @@ -203,7 +185,7 @@ public inline fun ResourceTry.flatMapCatching( * or falls back on UTF-8. */ public suspend fun Resource.readAsString(charset: Charset? = null): ResourceTry = - read().mapCatching(this) { + read().mapCatching { String(it, charset = charset ?: Charsets.UTF_8) } @@ -211,19 +193,19 @@ public suspend fun Resource.readAsString(charset: Charset? = null): ResourceTry< * Reads the full content as a JSON object. */ public suspend fun Resource.readAsJson(): ResourceTry = - readAsString(charset = Charsets.UTF_8).mapCatching(this) { JSONObject(it) } + readAsString(charset = Charsets.UTF_8).mapCatching { JSONObject(it) } /** * Reads the full content as an XML document. */ public suspend fun Resource.readAsXml(): ResourceTry = - read().mapCatching(this) { XmlParser().parse(ByteArrayInputStream(it)) } + read().mapCatching { XmlParser().parse(ByteArrayInputStream(it)) } /** * Reads the full content as a [Bitmap]. */ public suspend fun Resource.readAsBitmap(): ResourceTry = - read().mapCatching(this) { + read().mapCatching { BitmapFactory.decodeByteArray(it, 0, it.size) ?: throw kotlin.Exception("Could not decode resource as a bitmap") } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt index a228b5858e..a7fa47e118 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt @@ -42,7 +42,7 @@ public class RoutingContainer(private val routes: List) : Container { override fun get(url: Url): Container.Entry = routes.firstOrNull { it.accepts(url) }?.container?.get(url) - ?: FailureResource(Resource.Exception.NotFound(url)).toEntry(url) + ?: FailureResource(Resource.Exception.NotFound()).toEntry(url) override suspend fun close() { routes.forEach { it.container.close() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt index 26f1769c8b..2b8bd22bdd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt @@ -101,13 +101,13 @@ internal class JavaZipContainer( ) override suspend fun properties(): ResourceTry = - Try.failure(Resource.Exception.NotFound(url)) + Try.failure(Resource.Exception.NotFound()) override suspend fun length(): ResourceTry = - Try.failure(Resource.Exception.NotFound(url)) + Try.failure(Resource.Exception.NotFound()) override suspend fun read(range: LongRange?): ResourceTry = - Try.failure(Resource.Exception.NotFound(url)) + Try.failure(Resource.Exception.NotFound()) override suspend fun close() { } @@ -139,7 +139,7 @@ internal class JavaZipContainer( override suspend fun length(): Try = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(Resource.Exception.Other(url, Exception("Unsupported operation"))) + ?: Try.failure(Resource.Exception.Other(Exception("Unsupported operation"))) private val compressedLength: Long? = if (entry.method == ZipEntry.STORED || entry.method == -1) { @@ -160,9 +160,9 @@ internal class JavaZipContainer( Try.success(bytes) } } catch (e: IOException) { - Try.failure(Resource.Exception.Unavailable(url, e)) + Try.failure(Resource.Exception.Unavailable(e)) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(url, e)) + Try.failure(Resource.Exception.wrap(e)) } private suspend fun readFully(): ByteArray = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index 07fa308357..8de612337e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -58,7 +58,7 @@ public class HtmlResourceContentExtractor : ResourceContentExtractor { override suspend fun extractText(resource: Resource): ResourceTry = withContext( Dispatchers.IO ) { - resource.readAsString().mapCatching(resource) { html -> + resource.readAsString().mapCatching { html -> val body = Jsoup.parse(html).body().text() // Transform HTML entities into their actual characters. Parser.unescapeEntities(body, false) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index 706040cc72..80eecf39be 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -37,7 +37,7 @@ internal class ChannelZipContainer( private inner class FailureEntry( override val url: Url - ) : Container.Entry, Resource by FailureResource(Resource.Exception.NotFound(url)) + ) : Container.Entry, Resource by FailureResource(Resource.Exception.NotFound()) private inner class Entry( override val url: Url, @@ -68,7 +68,7 @@ internal class ChannelZipContainer( override suspend fun length(): ResourceTry = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(Resource.Exception.Other(url, UnsupportedOperationException())) + ?: Try.failure(Resource.Exception.Other(UnsupportedOperationException())) private val compressedLength: Long? get() = @@ -89,7 +89,7 @@ internal class ChannelZipContainer( } Try.success(bytes) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(url, e)) + Try.failure(Resource.Exception.wrap(e)) } } From 3ec76f4f070aa5861aa3bb8e56dc247c81e6640a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Thu, 5 Oct 2023 10:07:28 +0200 Subject: [PATCH 05/11] Fix parsing of percent-decoded hrefs in the EPUB parser --- .../streamer/parser/epub/EncryptionParser.kt | 3 ++- .../r2/streamer/parser/epub/EpubParser.kt | 3 ++- .../r2/streamer/parser/epub/MetadataParser.kt | 3 ++- .../parser/epub/NavigationDocumentParser.kt | 3 ++- .../r2/streamer/parser/epub/NcxParser.kt | 3 ++- .../streamer/parser/epub/PackageDocument.kt | 3 ++- .../r2/streamer/parser/epub/SmilParser.kt | 9 +++++---- .../streamer/parser/epub/extensions/UrlExt.kt | 20 +++++++++++++++++++ 8 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/extensions/UrlExt.kt diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt index 76a0f5fdf4..0ac575dcfe 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt @@ -10,6 +10,7 @@ import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal object EncryptionParser { fun parse(document: ElementNode): Map = @@ -20,7 +21,7 @@ internal object EncryptionParser { private fun parseEncryptedData(node: ElementNode): Pair? { val resourceURI = node.getFirst("CipherData", Namespaces.ENC) ?.getFirst("CipherReference", Namespaces.ENC)?.getAttr("URI") - ?.let { Url(it) } + ?.let { Url.fromEpubHref(it) } ?: return null val retrievalMethod = node.getFirst("KeyInfo", Namespaces.SIG) ?.getFirst("RetrievalMethod", Namespaces.SIG)?.getAttr("URI") diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index abbf0ec91d..63a94d3e65 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -26,6 +26,7 @@ import org.readium.r2.shared.util.resource.readAsXml import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.readAsXmlOrNull import org.readium.r2.streamer.parser.PublicationParser +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref /** * Parses a Publication from an EPUB publication. @@ -98,7 +99,7 @@ public class EpubParser( .getFirst("rootfiles", Namespaces.OPC) ?.getFirst("rootfile", Namespaces.OPC) ?.getAttr("full-path") - ?.let { Url(it) } + ?.let { Url.fromEpubHref(it) } ?.let { Try.success(it) } ?: Try.failure(PublicationParser.Error.ParsingFailed("Cannot successfully parse OPF.")) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt index c7ea14781d..568e345d88 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt @@ -11,6 +11,7 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal class MetadataParser( private val prefixMap: Map, @@ -38,7 +39,7 @@ internal class MetadataParser( } private fun parseLinkElement(element: ElementNode, filePath: Url): MetadataItem.Link? { - val href = element.getAttr("href")?.let { Url(it) } ?: return null + val href = element.getAttr("href")?.let { Url.fromEpubHref(it) } ?: return null val relAttr = element.getAttr("rel").orEmpty() val rel = parseProperties(relAttr).map { resolveProperty(it, prefixMap, DEFAULT_VOCAB.LINK) } val propAttr = element.getAttr("properties").orEmpty() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt index 2680857751..dfba4e8482 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt @@ -9,6 +9,7 @@ package org.readium.r2.streamer.parser.epub import org.readium.r2.shared.publication.Link import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal object NavigationDocumentParser { @@ -64,7 +65,7 @@ internal object NavigationDocumentParser { " " ).trim() } - val rawHref = first.getAttr("href")?.let { Url(it) } + val rawHref = first.getAttr("href")?.let { Url.fromEpubHref(it) } val href = if (first.name == "a" && rawHref != null) { filePath.resolve(rawHref) } else { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NcxParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NcxParser.kt index 86dbaac117..a0db885df4 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NcxParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NcxParser.kt @@ -9,6 +9,7 @@ package org.readium.r2.streamer.parser.epub import org.readium.r2.shared.publication.Link import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal object NcxParser { @@ -61,6 +62,6 @@ internal object NcxParser { private fun extractHref(element: ElementNode, filePath: Url) = element.getFirst("content", Namespaces.NCX)?.getAttr("src") ?.ifBlank { null } - ?.let { Url(it) } + ?.let { Url.fromEpubHref(it) } ?.let { filePath.resolve(it) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt index 3654281d94..aa3920a03f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt @@ -10,6 +10,7 @@ import org.readium.r2.shared.publication.ReadingProgression import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal data class PackageDocument( val path: Url, @@ -56,7 +57,7 @@ internal data class Item( companion object { fun parse(element: ElementNode, filePath: Url, prefixMap: Map): Item? { val href = element.getAttr("href") - ?.let { Url(it) } + ?.let { Url.fromEpubHref(it) } ?.let { filePath.resolve(it) } ?: return null val propAttr = element.getAttr("properties").orEmpty() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/SmilParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/SmilParser.kt index 4bc4834b9a..460d8b8007 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/SmilParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/SmilParser.kt @@ -10,6 +10,7 @@ import org.readium.r2.shared.MediaOverlayNode import org.readium.r2.shared.MediaOverlays import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal object SmilParser { /* According to https://www.w3.org/publishing/epub3/epub-mediaoverlays.html#sec-overlays-content-conf @@ -39,7 +40,7 @@ internal object SmilParser { - the seq element has an textref attribute (this is mandatory according to the EPUB spec) */ val textref = node.getAttrNs("textref", Namespaces.OPS) - ?.let { Url(it) } + ?.let { Url.fromEpubHref(it) } val audioFiles = children.mapNotNull(MediaOverlayNode::audioFile) return if (textref != null && audioFiles.distinct().size == 1) { // hierarchy val normalizedTextref = filePath.resolve(textref) @@ -52,7 +53,7 @@ internal object SmilParser { private fun parsePar(node: ElementNode, filePath: Url): MediaOverlayNode? { val text = node.getFirst("text", Namespaces.SMIL) ?.getAttr("src") - ?.let { Url(it) } + ?.let { Url.fromEpubHref(it) } ?: return null val audio = node.getFirst("audio", Namespaces.SMIL) ?.let { audioNode -> @@ -61,7 +62,7 @@ internal object SmilParser { val end = audioNode.getAttr("clipEnd")?.let { ClockValueParser.parse(it) } ?: "" "$src#t=$begin,$end" } - ?.let { Url(it) } + ?.let { Url.fromEpubHref(it) } return MediaOverlayNode( filePath.resolve(text), @@ -75,7 +76,7 @@ internal object SmilParser { val file = audioChildren.first().audioFile val start = audioChildren.first().clip.start ?: "" val end = audioChildren.last().clip.end ?: "" - val audio = Url("$file#t=$start,$end") + val audio = Url.fromEpubHref("$file#t=$start,$end") return MediaOverlayNode(text, audio, children, listOf("section")) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/extensions/UrlExt.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/extensions/UrlExt.kt new file mode 100644 index 0000000000..4280496e76 --- /dev/null +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/extensions/UrlExt.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.streamer.parser.epub.extensions + +import org.readium.r2.shared.util.Url + +/** + * According to the EPUB specification, the HREFs in the EPUB package must be valid URLs (so + * percent-encoded). Unfortunately, many EPUBs don't follow this rule, and use invalid HREFs such + * as `my chapter.html` or `/dir/my chapter.html`. + * + * As a workaround, we assume the HREFs are valid percent-encoded URLs, and fallback to decoded paths + * if we can't parse the URL. + */ +internal fun Url.Companion.fromEpubHref(href: String): Url? = + Url(href) ?: fromDecodedPath(href) From 47d4a2e2550fdb96244659a77736ec6bd17d1eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Thu, 5 Oct 2023 10:37:05 +0200 Subject: [PATCH 06/11] Fix source of ANR in `HtmlResourceContentIterator` --- .../iterators/HtmlResourceContentIterator.kt | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index fe97aaae2d..75e0ef1f21 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -6,6 +6,8 @@ package org.readium.r2.shared.publication.services.content.iterators +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.jsoup.Jsoup import org.jsoup.nodes.Element import org.jsoup.nodes.Node @@ -146,43 +148,44 @@ public class HtmlResourceContentIterator internal constructor( private var parsedElements: ParsedElements? = null - private suspend fun parseElements(): ParsedElements { - val document = resource.use { res -> - val html = res.readAsString().getOrElse { - Timber.w(it, "Failed to read HTML resource") - return ParsedElements() + private suspend fun parseElements(): ParsedElements = + withContext(Dispatchers.Default) { + val document = resource.use { res -> + val html = res.readAsString().getOrElse { + Timber.w(it, "Failed to read HTML resource") + return@withContext ParsedElements() + } + + Jsoup.parse(html) } - Jsoup.parse(html) - } + val contentParser = ContentParser( + baseLocator = locator, + startElement = locator.locations.cssSelector?.let { + tryOrNull { document.selectFirst(it) } + }, + beforeMaxLength = beforeMaxLength + ) + NodeTraversor.traverse(contentParser, document.body()) + val elements = contentParser.result() + val elementCount = elements.elements.size + if (elementCount == 0) { + return@withContext elements + } - val contentParser = ContentParser( - baseLocator = locator, - startElement = locator.locations.cssSelector?.let { - tryOrNull { document.selectFirst(it) } - }, - beforeMaxLength = beforeMaxLength - ) - NodeTraversor.traverse(contentParser, document.body()) - val elements = contentParser.result() - val elementCount = elements.elements.size - if (elementCount == 0) { - return elements + elements.copy( + elements = elements.elements.mapIndexed { index, element -> + val progression = index.toDouble() / elementCount + element.copy( + progression = progression, + totalProgression = totalProgressionRange?.let { + totalProgressionRange.start + progression * (totalProgressionRange.endInclusive - totalProgressionRange.start) + } + ) + } + ) } - return elements.copy( - elements = elements.elements.mapIndexed { index, element -> - val progression = index.toDouble() / elementCount - element.copy( - progression = progression, - totalProgression = totalProgressionRange?.let { - totalProgressionRange.start + progression * (totalProgressionRange.endInclusive - totalProgressionRange.start) - } - ) - } - ) - } - private fun Content.Element.copy(progression: Double?, totalProgression: Double?): Content.Element { fun Locator.update(): Locator = copyWithLocations( From d0be4c9b69801d7a565cfde1efc179c6459043cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Thu, 5 Oct 2023 10:56:50 +0200 Subject: [PATCH 07/11] Optimize opening an EPUB Don't check for a non-empty content iterator when creating the TTS navigator factory --- .../navigator/media/tts/TtsNavigator.kt | 3 +++ .../media/tts/TtsNavigatorFactory.kt | 16 ++++++---------- .../media/tts/TtsUtteranceIterator.kt | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt index 6fada09064..7f59d6c7ad 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt @@ -76,6 +76,9 @@ public class TtsNavigator, val contentIterator = TtsUtteranceIterator(publication, tokenizerFactory, initialLocator) + if (!contentIterator.hasNext()) { + return null + } val ttsEngine = ttsEngineProvider.createEngine(publication, actualInitialPreferences) diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt index 0bebd6f4bb..53ed0398ec 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt @@ -34,7 +34,7 @@ public class TtsNavigatorFactory TextTokenizer = defaultTokenizerFactory, @@ -57,24 +57,23 @@ public class TtsNavigatorFactory, E : PreferencesEditor

, + public operator fun , E : PreferencesEditor

, F : TtsEngine.Error, V : TtsEngine.Voice> invoke( application: Application, publication: Publication, ttsEngineProvider: TtsEngineProvider, tokenizerFactory: (language: Language?) -> TextTokenizer = defaultTokenizerFactory, metadataProvider: MediaMetadataProvider = defaultMediaMetadataProvider - ): TtsNavigatorFactory? { - return createNavigatorFactory( + ): TtsNavigatorFactory? = + createNavigatorFactory( application, publication, ttsEngineProvider, tokenizerFactory, metadataProvider ) - } - private suspend fun , E : PreferencesEditor

, + private fun , E : PreferencesEditor

, F : TtsEngine.Error, V : TtsEngine.Voice> createNavigatorFactory( application: Application, publication: Publication, @@ -82,10 +81,7 @@ public class TtsNavigatorFactory TextTokenizer, metadataProvider: MediaMetadataProvider ): TtsNavigatorFactory? { - publication.content() - ?.iterator() - ?.takeIf { it.hasNext() } - ?: return null + publication.content() ?: return null return TtsNavigatorFactory( application, diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsUtteranceIterator.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsUtteranceIterator.kt index 83f7be7888..f3179ab5a7 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsUtteranceIterator.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsUtteranceIterator.kt @@ -105,6 +105,19 @@ internal class TtsUtteranceIterator( private fun createIterator(locator: Locator?): Content.Iterator = contentService.content(locator).iterator() + suspend fun hasPrevious(): Boolean = + hasNextIn(Direction.Backward) + + suspend fun hasNext(): Boolean = + hasNextIn(Direction.Forward) + + private suspend fun hasNextIn(direction: Direction): Boolean { + if (utterances.isEmpty()) { + loadNextUtterances(direction) + } + return utterances.hasNextIn(direction) + } + /** * Advances to the previous item and returns it, or null if we reached the beginning. */ @@ -217,6 +230,12 @@ internal class TtsUtteranceIterator( } } + private fun CursorList.hasNextIn(direction: Direction): Boolean = + when (direction) { + Direction.Forward -> hasNext() + Direction.Backward -> hasPrevious() + } + private fun CursorList.nextIn(direction: Direction): E? = when (direction) { Direction.Forward -> if (hasNext()) next() else null From 17a768e18d8b7543fa605d26273bdfba489cb7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Thu, 5 Oct 2023 13:28:07 +0200 Subject: [PATCH 08/11] Add a guard when restoring a PSPDFKit page index --- .../pspdfkit/navigator/PsPdfKitDocumentFragment.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt index fb668f2757..6ba6f00b01 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt @@ -57,6 +57,7 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.pdf.cachedIn import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry +import timber.log.Timber @ExperimentalReadiumApi public class PsPdfKitDocumentFragment internal constructor( @@ -295,9 +296,15 @@ public class PsPdfKitDocumentFragment internal constructor( } override fun onDocumentLoaded(document: PdfDocument) { - super.onDocumentLoaded(document) + val index = pageIndex.value + if (index < 0 || index >= document.pageCount) { + Timber.w( + "Tried to restore page index $index, but the document has ${document.pageCount} pages" + ) + return + } - checkNotNull(pdfFragment).setPageIndex(pageIndex.value, false) + checkNotNull(pdfFragment).setPageIndex(index, false) } } From 58ac661a1baecce2a928d80b72bd5d64da0bc7a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Thu, 5 Oct 2023 13:39:51 +0200 Subject: [PATCH 09/11] Guard against crashes with JSoup's `cssSelector()` --- .../iterators/HtmlResourceContentIterator.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index 75e0ef1f21..a46ee6bf12 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -16,6 +16,7 @@ import org.jsoup.parser.Parser import org.jsoup.select.NodeTraversor import org.jsoup.select.NodeVisitor import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator @@ -259,9 +260,12 @@ public class HtmlResourceContentIterator internal constructor( private data class ParentElement( val element: Element, - val cssSelector: String + val cssSelector: String? ) { - constructor(element: Element) : this(element, element.cssSelector()) + constructor(element: Element) : this( + element = element, + cssSelector = tryOrLog { element.cssSelector() } + ) } override fun head(node: Node, depth: Int) { @@ -278,7 +282,9 @@ public class HtmlResourceContentIterator internal constructor( baseLocator.copy( locations = Locator.Locations( otherLocations = buildMap { - put("cssSelector", parent.cssSelector as Any) + parent.cssSelector?.let { + put("cssSelector", it as Any) + } } ) ) @@ -405,8 +411,8 @@ public class HtmlResourceContentIterator internal constructor( locator = baseLocator.copy( locations = Locator.Locations( otherLocations = buildMap { - parent?.let { - put("cssSelector", it.cssSelector as Any) + parent?.cssSelector?.let { + put("cssSelector", it as Any) } } ), @@ -445,8 +451,8 @@ public class HtmlResourceContentIterator internal constructor( locator = baseLocator.copy( locations = Locator.Locations( otherLocations = buildMap { - parent?.let { - put("cssSelector", it.cssSelector as Any) + parent?.cssSelector?.let { + put("cssSelector", it as Any) } } ), From fd112920499689abfdfe528a0953221fc80dd1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Thu, 5 Oct 2023 14:39:55 +0200 Subject: [PATCH 10/11] Prevent internal crashes caused by JavaZip --- .../r2/shared/util/resource/ZipContainer.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt index 2b8bd22bdd..4994438a98 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt @@ -210,17 +210,21 @@ internal class JavaZipContainer( override val source: AbsoluteUrl = file.toUrl() override suspend fun entries(): Set = - archive.entries().toList() - .filterNot { it.isDirectory } - .mapNotNull { entry -> - Url.fromDecodedPath(entry.name) - ?.let { url -> Entry(url, entry) } - } - .toSet() + tryOrLog { + archive.entries().toList() + .filterNot { it.isDirectory } + .mapNotNull { entry -> + Url.fromDecodedPath(entry.name) + ?.let { url -> Entry(url, entry) } + } + .toSet() + } ?: emptySet() override fun get(url: Url): Container.Entry = (url as? RelativeUrl)?.path - ?.let { archive.getEntry(it) } + ?.let { + tryOrLog { archive.getEntry(it) } + } ?.let { Entry(url, it) } ?: FailureEntry(url) From ff3ca1365c1b3a65fdf401c071a90f30ee712c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Fri, 13 Oct 2023 09:08:29 +0200 Subject: [PATCH 11/11] Address review comment --- .../java/org/readium/r2/shared/util/resource/ZipContainer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt index 4994438a98..dda194d101 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt @@ -209,7 +209,7 @@ internal class JavaZipContainer( override val source: AbsoluteUrl = file.toUrl() - override suspend fun entries(): Set = + override suspend fun entries(): Set? = tryOrLog { archive.entries().toList() .filterNot { it.isDirectory } @@ -218,7 +218,7 @@ internal class JavaZipContainer( ?.let { url -> Entry(url, entry) } } .toSet() - } ?: emptySet() + } override fun get(url: Url): Container.Entry = (url as? RelativeUrl)?.path