Skip to content

Commit

Permalink
Abstract the toolkit over publication source (#353)
Browse files Browse the repository at this point in the history
  • Loading branch information
qnga authored Jul 17, 2023
1 parent 2d80205 commit 6a489f7
Show file tree
Hide file tree
Showing 196 changed files with 5,723 additions and 4,655 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import kotlin.reflect.KClass
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.readium.r2.shared.PdfSupport
import org.readium.r2.shared.error.getOrThrow
import org.readium.r2.shared.extensions.md5
import org.readium.r2.shared.extensions.tryOrNull
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.util.pdf.PdfDocument
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
import org.readium.r2.shared.util.use
Expand Down Expand Up @@ -89,19 +90,19 @@ class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory<PdfiumDocumen
override suspend fun open(file: File, password: String?): PdfiumDocument =
core.fromFile(file, password)

override suspend fun open(resource: Resource, password: String?): PdfiumDocument {
override suspend fun open(resource: Fetcher.Resource, password: String?): PdfiumDocument {
// First try to open the resource as a file on the FS for performance improvement, as
// PDFium requires the whole PDF document to be loaded in memory when using raw bytes.
return resource.openAsFile(password)
?: resource.openBytes(password)
}

private suspend fun Resource.openAsFile(password: String?): PdfiumDocument? =
private suspend fun Fetcher.Resource.openAsFile(password: String?): PdfiumDocument? =
file?.let {
tryOrNull { open(it, password) }
}

private suspend fun Resource.openBytes(password: String?): PdfiumDocument =
private suspend fun Fetcher.Resource.openBytes(password: String?): PdfiumDocument =
use {
core.fromBytes(read().getOrThrow(), password)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import org.readium.r2.navigator.preferences.Axis
import org.readium.r2.navigator.preferences.Fit
import org.readium.r2.navigator.preferences.ReadingProgression
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.resource.Resource
import timber.log.Timber

@ExperimentalReadiumApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.io.File
import kotlin.reflect.KClass
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.publication.ReadingProgression
import org.readium.r2.shared.util.pdf.PdfDocument
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
Expand All @@ -33,7 +33,7 @@ class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory<PsPdfKitDoc
override suspend fun open(file: File, password: String?): PsPdfKitDocument =
open(context, DocumentSource(file.toUri(), password))

override suspend fun open(resource: Resource, password: String?): PsPdfKitDocument =
override suspend fun open(resource: Fetcher.Resource, password: String?): PsPdfKitDocument =
open(context, DocumentSource(ResourceDataProvider(resource), password))

private suspend fun open(context: Context, documentSource: DocumentSource): PsPdfKitDocument =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ package org.readium.adapters.pspdfkit.document
import com.pspdfkit.document.providers.DataProvider
import java.util.*
import kotlinx.coroutines.runBlocking
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.error.getOrElse
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.fetcher.synchronized
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.util.isLazyInitialized
import timber.log.Timber

class ResourceDataProvider(
resource: Resource,
resource: Fetcher.Resource,
private val onResourceError: (Resource.Exception) -> Unit = { Timber.e(it) }
) : DataProvider {

Expand Down
195 changes: 144 additions & 51 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,94 +7,187 @@
package org.readium.r2.lcp

import org.readium.r2.lcp.auth.LcpPassphraseAuthentication
import org.readium.r2.lcp.service.LcpLicensedAsset
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.asset.Asset
import org.readium.r2.shared.error.ThrowableError
import org.readium.r2.shared.error.Try
import org.readium.r2.shared.error.getOrElse
import org.readium.r2.shared.fetcher.ContainerFetcher
import org.readium.r2.shared.fetcher.TransformingFetcher
import org.readium.r2.shared.publication.ContentProtection
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.publication.asset.FileAsset
import org.readium.r2.shared.publication.asset.PublicationAsset
import org.readium.r2.shared.publication.asset.RemoteAsset
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.resource.ArchiveFactory
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.resource.ResourceFactory
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever

internal class LcpContentProtection(
private val lcpService: LcpService,
private val authentication: LcpAuthenticating
private val authentication: LcpAuthenticating,
private val mediaTypeRetriever: MediaTypeRetriever,
private val resourceFactory: ResourceFactory,
private val archiveFactory: ArchiveFactory
) : ContentProtection {

override val scheme: ContentProtection.Scheme =
ContentProtection.Scheme.Lcp

override suspend fun supports(
asset: Asset
): Boolean =
lcpService.isLcpProtected(asset)

override suspend fun open(
asset: PublicationAsset,
asset: Asset,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.ProtectedAsset, Publication.OpeningException>? {
): Try<ContentProtection.Asset, Publication.OpeningException> {
return when (asset) {
is Asset.Container -> openPublication(asset, credentials, allowUserInteraction, sender)
is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction, sender)
}
}

private suspend fun openPublication(
asset: Asset.Container,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
val license = retrieveLicense(asset, credentials, allowUserInteraction, sender)
?: return null
return createProtectedAsset(asset, license)
return createResultAsset(asset, license)
}

/* Returns null if the publication is not protected by LCP. */
private suspend fun retrieveLicense(
asset: PublicationAsset,
asset: Asset,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<LcpLicense, LcpException>? {

): Try<LcpLicense, LcpException> {
val authentication = credentials
?.let { LcpPassphraseAuthentication(it, fallback = this.authentication) }
?: this.authentication

val license = when (asset) {
is FileAsset ->
lcpService.retrieveLicense(asset.file, authentication, allowUserInteraction, sender)
is RemoteAsset ->
lcpService.retrieveLicense(asset.fetcher, asset.mediaType, authentication, allowUserInteraction, sender)
is LcpLicensedAsset ->
asset.license
?.let { Try.success(it) }
?: lcpService.retrieveLicense(asset.licenseFile, authentication, allowUserInteraction, sender)
else ->
null
}
val file = (asset as? Asset.Resource)?.resource?.file
?: (asset as? Asset.Container)?.container?.file

return license?.takeUnless { result ->
result is Try.Failure<*, *> && result.exception is LcpException.Container
}
return file
// This is less restrictive with regard to network availability.
?.let { lcpService.retrieveLicense(it, asset.mediaType, authentication, allowUserInteraction, sender) }
?: lcpService.retrieveLicense(asset, authentication, allowUserInteraction, sender)
}

private fun createProtectedAsset(
originalAsset: PublicationAsset,
private fun createResultAsset(
asset: Asset.Container,
license: Try<LcpLicense, LcpException>,
): Try<ContentProtection.ProtectedAsset, Publication.OpeningException> {
): Try<ContentProtection.Asset, Publication.OpeningException> {
val serviceFactory = LcpContentProtectionService
.createFactory(license.getOrNull(), license.exceptionOrNull())
.createFactory(license.getOrNull(), license.failureOrNull())

val newFetcher = TransformingFetcher(
originalAsset.fetcher,
val fetcher = TransformingFetcher(
ContainerFetcher(asset.container, mediaTypeRetriever),
LcpDecryptor(license.getOrNull())::transform
)

val newAsset = when (originalAsset) {
is FileAsset -> {
originalAsset.copy(fetcher = newFetcher)
}
is RemoteAsset -> {
originalAsset.copy(fetcher = newFetcher)
}
is LcpLicensedAsset -> {
originalAsset.copy(fetcher = newFetcher)
}
else -> throw IllegalStateException()
}

val protectedFile = ContentProtection.ProtectedAsset(
asset = newAsset,
val protectedFile = ContentProtection.Asset(
name = asset.name,
mediaType = asset.mediaType,
fetcher = fetcher,
onCreatePublication = {
servicesBuilder.contentProtectionServiceFactory = serviceFactory
}
)

return Try.success(protectedFile)
}

private suspend fun openLicense(
licenseAsset: Asset.Resource,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction, sender)

val licenseDoc = license.getOrNull()?.license
?: licenseAsset.resource.read()
.map {
try {
LicenseDocument(it)
} catch (e: Exception) {
return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(e)
)
)
}
}
.getOrElse {
return Try.failure(
it.wrap()
)
}

val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.publication))
val url = Url(link.url.toString())
?: return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(
LcpException.Parsing.Url(rel = LicenseDocument.Rel.publication.rawValue)
)
)
)

val resource = resourceFactory.create(url)
.getOrElse { return Try.failure(it.wrap()) }

val container = archiveFactory.create(resource, password = null)
.getOrElse { return Try.failure(it.wrap()) }

val publicationAsset = Asset.Container(
url.filename,
link.mediaType,
false,
container
)

return createResultAsset(publicationAsset, license)
}

private fun ResourceFactory.Error.wrap(): Publication.OpeningException =
when (this) {
is ResourceFactory.Error.NotAResource ->
Publication.OpeningException.NotFound()
is ResourceFactory.Error.Forbidden ->
Publication.OpeningException.Forbidden()
is ResourceFactory.Error.SchemeNotSupported ->
Publication.OpeningException.UnsupportedAsset()
}

private fun ArchiveFactory.Error.wrap(): Publication.OpeningException =
when (this) {
is ArchiveFactory.Error.FormatNotSupported ->
Publication.OpeningException.UnsupportedAsset()
is ArchiveFactory.Error.PasswordsNotSupported ->
Publication.OpeningException.UnsupportedAsset()
is ArchiveFactory.Error.ResourceReading ->
resourceException.wrap()
}

private fun Resource.Exception.wrap(): Publication.OpeningException =
when (this) {
is Resource.Exception.Forbidden ->
Publication.OpeningException.Forbidden(ThrowableError(this))
is Resource.Exception.NotFound ->
Publication.OpeningException.NotFound(ThrowableError(this))
Resource.Exception.Offline, is Resource.Exception.Unavailable ->
Publication.OpeningException.Unavailable(ThrowableError(this))
is Resource.Exception.Other, is Resource.Exception.BadRequest ->
Publication.OpeningException.Unexpected(this)
is Resource.Exception.OutOfMemory ->
Publication.OpeningException.OutOfMemory(ThrowableError(this))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

package org.readium.r2.lcp

import org.readium.r2.shared.publication.ContentProtection
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.publication.services.ContentProtectionService

class LcpContentProtectionService(val license: LcpLicense?, override val error: LcpException?) : ContentProtectionService {
Expand Down
15 changes: 9 additions & 6 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,24 @@
package org.readium.r2.lcp

import java.io.IOException
import org.readium.r2.shared.error.Try
import org.readium.r2.shared.error.getOrElse
import org.readium.r2.shared.error.getOrThrow
import org.readium.r2.shared.extensions.coerceFirstNonNegative
import org.readium.r2.shared.extensions.inflate
import org.readium.r2.shared.extensions.requireLengthFitInt
import org.readium.r2.shared.fetcher.*
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.encryption.encryption
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.resource.ResourceTry

/**
* Decrypts a resource protected with LCP.
*/
internal class LcpDecryptor(val license: LcpLicense?) {

fun transform(resource: Resource): Resource = LazyResource {
fun transform(resource: Fetcher.Resource): Fetcher.Resource = LazyResource {
// Checks if the resource is encrypted and whether the encryption schemes of the resource
// and the DRM license are the same.
val link = resource.link()
Expand All @@ -46,7 +49,7 @@ internal class LcpDecryptor(val license: LcpLicense?) {
* resource, for example when the resource is deflated before encryption.
*/
private class FullLcpResource(
resource: Resource,
resource: Fetcher.Resource,
private val license: LcpLicense
) : TransformingResource(resource) {

Expand All @@ -65,9 +68,9 @@ internal class LcpDecryptor(val license: LcpLicense?) {
* Supports random access for byte range requests, but the resource MUST NOT be deflated.
*/
private class CbcLcpResource(
private val resource: Resource,
private val resource: Fetcher.Resource,
private val license: LcpLicense
) : Resource {
) : Fetcher.Resource {

private class Cache(
var startIndex: Int? = null,
Expand Down
Loading

0 comments on commit 6a489f7

Please sign in to comment.