Skip to content

Commit

Permalink
Various changes including introducing Format (readium#427)
Browse files Browse the repository at this point in the history
  • Loading branch information
qnga authored Dec 21, 2023
1 parent c9a09ac commit bfadfa8
Show file tree
Hide file tree
Showing 121 changed files with 3,760 additions and 3,692 deletions.
107 changes: 74 additions & 33 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,50 @@ package org.readium.r2.lcp

import org.readium.r2.lcp.auth.LcpPassphraseAuthentication
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.publication.encryption.Encryption
import org.readium.r2.shared.publication.encryption.encryption
import org.readium.r2.shared.publication.flatten
import org.readium.r2.shared.publication.epub.EpubEncryptionParser
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.DebugError
import org.readium.r2.shared.util.ThrowableError
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.asset.Asset
import org.readium.r2.shared.util.asset.AssetRetriever
import org.readium.r2.shared.util.asset.AssetOpener
import org.readium.r2.shared.util.asset.ContainerAsset
import org.readium.r2.shared.util.asset.ResourceAsset
import org.readium.r2.shared.util.data.Container
import org.readium.r2.shared.util.data.ReadError
import org.readium.r2.shared.util.data.decodeRwpm
import org.readium.r2.shared.util.data.decodeXml
import org.readium.r2.shared.util.data.readDecodeOrElse
import org.readium.r2.shared.util.flatMap
import org.readium.r2.shared.util.format.Format
import org.readium.r2.shared.util.format.Trait
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.util.resource.Resource
import org.readium.r2.shared.util.resource.TransformingContainer

internal class LcpContentProtection(
private val lcpService: LcpService,
private val authentication: LcpAuthenticating,
private val assetRetriever: AssetRetriever
private val assetOpener: AssetOpener
) : ContentProtection {

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

override suspend fun supports(
asset: Asset
): Try<Boolean, Nothing> =
Try.success(lcpService.isLcpProtected(asset))

override suspend fun open(
asset: Asset,
credentials: String?,
allowUserInteraction: Boolean
): Try<ContentProtection.Asset, ContentProtection.OpenError> {
): Try<ContentProtection.OpenResult, ContentProtection.OpenError> {
if (
!asset.format.conformsTo(Trait.LCP_PROTECTED) &&
!asset.format.conformsTo(Format.LCP_LICENSE_DOCUMENT)
) {
return Try.failure(ContentProtection.OpenError.AssetNotSupported())
}

return when (asset) {
is ContainerAsset -> openPublication(asset, credentials, allowUserInteraction)
is ResourceAsset -> openLicense(asset, credentials, allowUserInteraction)
Expand All @@ -54,7 +62,7 @@ internal class LcpContentProtection(
asset: ContainerAsset,
credentials: String?,
allowUserInteraction: Boolean
): Try<ContentProtection.Asset, ContentProtection.OpenError> {
): Try<ContentProtection.OpenResult, ContentProtection.OpenError> {
val license = retrieveLicense(asset, credentials, allowUserInteraction)
return createResultAsset(asset, license)
}
Expand All @@ -71,40 +79,73 @@ internal class LcpContentProtection(
return lcpService.retrieveLicense(asset, authentication, allowUserInteraction)
}

private fun createResultAsset(
private suspend fun createResultAsset(
asset: ContainerAsset,
license: Try<LcpLicense, LcpError>
): Try<ContentProtection.Asset, ContentProtection.OpenError> {
): Try<ContentProtection.OpenResult, ContentProtection.OpenError> {
val serviceFactory = LcpContentProtectionService
.createFactory(license.getOrNull(), license.failureOrNull())

val decryptor = LcpDecryptor(license.getOrNull())
val encryptionData =
when {
asset.format.conformsTo(Trait.EPUB) -> parseEncryptionDataEpub(asset.container)
else -> parseEncryptionDataRpf(asset.container)
}
.getOrElse { return Try.failure(ContentProtection.OpenError.Reading(it)) }

val decryptor = LcpDecryptor(license.getOrNull(), encryptionData)

val container = TransformingContainer(asset.container, decryptor::transform)

val protectedFile = ContentProtection.Asset(
mediaType = asset.mediaType,
container = container,
val protectedFile = ContentProtection.OpenResult(
asset = ContainerAsset(
format = asset.format,
container = container
),
onCreatePublication = {
decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links)
.flatten()
.mapNotNull {
it.properties.encryption?.let { enc -> it.url() to enc }
}
.toMap()

servicesBuilder.contentProtectionServiceFactory = serviceFactory
}
)

return Try.success(protectedFile)
}

private suspend fun parseEncryptionDataEpub(container: Container<Resource>): Try<Map<Url, Encryption>, ReadError> {
val encryptionResource = container[Url("META-INF/encryption.xml")!!]
?: return Try.failure(ReadError.Decoding("Missing encryption.xml"))

val encryptionDocument = encryptionResource
.readDecodeOrElse(
decode = { it.decodeXml() },
recover = { return Try.failure(it) }
)

return Try.success(EpubEncryptionParser.parse(encryptionDocument))
}

private suspend fun parseEncryptionDataRpf(container: Container<Resource>): Try<Map<Url, Encryption>, ReadError> {
val manifestResource = container[Url("manifest.json")!!]
?: return Try.failure(ReadError.Decoding("Missing manifest"))

val manifest = manifestResource
.readDecodeOrElse(
decode = { it.decodeRwpm() },
recover = { return Try.failure(it) }
)

val encryptionData = manifest
.let { (it.readingOrder + it.resources) }
.mapNotNull { link -> link.properties.encryption?.let { link.url() to it } }
.toMap()

return Try.success(encryptionData)
}

private suspend fun openLicense(
licenseAsset: ResourceAsset,
credentials: String?,
allowUserInteraction: Boolean
): Try<ContentProtection.Asset, ContentProtection.OpenError> {
): Try<ContentProtection.OpenResult, ContentProtection.OpenError> {
val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction)

val licenseDoc = license.getOrNull()?.license
Expand Down Expand Up @@ -145,14 +186,14 @@ internal class LcpContentProtection(

val asset =
if (link.mediaType != null) {
assetRetriever.retrieve(
assetOpener.open(
url,
mediaType = link.mediaType
)
.map { it as ContainerAsset }
.mapFailure { it.wrap() }
} else {
assetRetriever.retrieve(url)
assetOpener.open(url)
.mapFailure { it.wrap() }
.flatMap {
if (it is ContainerAsset) {
Expand All @@ -172,13 +213,13 @@ internal class LcpContentProtection(
return asset.flatMap { createResultAsset(it, license) }
}

private fun AssetRetriever.RetrieveError.wrap(): ContentProtection.OpenError =
private fun AssetOpener.OpenError.wrap(): ContentProtection.OpenError =
when (this) {
is AssetRetriever.RetrieveError.FormatNotSupported ->
is AssetOpener.OpenError.FormatNotSupported ->
ContentProtection.OpenError.AssetNotSupported(this)
is AssetRetriever.RetrieveError.Reading ->
is AssetOpener.OpenError.Reading ->
ContentProtection.OpenError.Reading(cause)
is AssetRetriever.RetrieveError.SchemeNotSupported ->
is AssetOpener.OpenError.SchemeNotSupported ->
ContentProtection.OpenError.AssetNotSupported(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import org.readium.r2.shared.util.resource.flatMap
*/
internal class LcpDecryptor(
val license: LcpLicense?,
var encryptionData: Map<Url, Encryption> = emptyMap()
val encryptionData: Map<Url, Encryption>
) {

fun transform(url: Url, resource: Resource): Resource {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@ import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.extensions.tryOrLog
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.ErrorException
import org.readium.r2.shared.util.asset.AssetSniffer
import org.readium.r2.shared.util.downloads.DownloadManager
import org.readium.r2.shared.util.format.Format
import org.readium.r2.shared.util.format.FormatHints
import org.readium.r2.shared.util.format.FormatRegistry
import org.readium.r2.shared.util.format.Trait
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.util.mediatype.FormatRegistry
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeHints
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever

/**
* Utility to acquire a protected publication from an LCP License Document.
*/
public class LcpPublicationRetriever(
context: Context,
private val downloadManager: DownloadManager,
private val mediaTypeRetriever: MediaTypeRetriever
private val assetSniffer: AssetSniffer
) {

@JvmInline
Expand Down Expand Up @@ -194,19 +195,22 @@ public class LcpPublicationRetriever(
}
downloadsRepository.removeDownload(requestId.value)

val mediaTypeWithoutLicense = mediaTypeRetriever.retrieve(
download.file,
MediaTypeHints(
mediaTypes = listOfNotNull(
license.publicationLink.mediaType,
download.mediaType
val baseFormat =
assetSniffer.sniff(
download.file,
FormatHints(
mediaTypes = listOfNotNull(
license.publicationLink.mediaType,
download.mediaType
)
)
)
).getOrElse { MediaType.EPUB }
).getOrElse { Format.EPUB }

val format = baseFormat + Trait.LCP_PROTECTED

try {
// Saves the License Document into the downloaded publication
val container = createLicenseContainer(download.file, mediaTypeWithoutLicense)
val container = createLicenseContainer(download.file, format)
container.write(license)
} catch (e: Exception) {
tryOrLog { download.file.delete() }
Expand All @@ -216,20 +220,10 @@ public class LcpPublicationRetriever(
return@launch
}

val mediaType = mediaTypeRetriever.retrieve(
download.file,
MediaTypeHints(
mediaTypes = listOfNotNull(
license.publicationLink.mediaType,
download.mediaType
)
)
).getOrElse { MediaType.EPUB }

val acquiredPublication = LcpService.AcquiredPublication(
localFile = download.file,
suggestedFilename = "${license.id}.${formatRegistry.fileExtension(mediaType) ?: "epub"}",
mediaType = mediaType,
suggestedFilename = "${license.id}.${format.fileExtension}",
format,
licenseDocument = license
)

Expand Down Expand Up @@ -285,4 +279,7 @@ public class LcpPublicationRetriever(
listeners.remove(lcpRequestId)
}
}

private val Format.fileExtension: String get() =
formatRegistry[this]?.fileExtension?.value ?: "epub"
}
31 changes: 15 additions & 16 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import org.readium.r2.lcp.service.PassphrasesService
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.asset.Asset
import org.readium.r2.shared.util.asset.AssetRetriever
import org.readium.r2.shared.util.asset.AssetOpener
import org.readium.r2.shared.util.asset.AssetSniffer
import org.readium.r2.shared.util.downloads.DownloadManager
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever
import org.readium.r2.shared.util.format.Format

/**
* Service used to acquire and open publications protected with LCP.
Expand All @@ -42,13 +42,12 @@ public interface LcpService {
/**
* Returns if the file is a LCP license document or a publication protected by LCP.
*/
@Deprecated(
"Use an AssetSniffer and check the returned format for Trait.LCP_PROTECTED",
level = DeprecationLevel.ERROR
)
public suspend fun isLcpProtected(file: File): Boolean

/**
* Returns if the asset is a LCP license document or a publication protected by LCP.
*/
public suspend fun isLcpProtected(asset: Asset): Boolean

/**
* Acquires a protected publication from a standalone LCPL's bytes.
*
Expand Down Expand Up @@ -92,7 +91,7 @@ public interface LcpService {
*/
public suspend fun retrieveLicense(
file: File,
mediaType: MediaType,
format: Format,
authentication: LcpAuthenticating,
allowUserInteraction: Boolean
): Try<LcpLicense, LcpError>
Expand Down Expand Up @@ -146,7 +145,7 @@ public interface LcpService {
public data class AcquiredPublication(
val localFile: File,
val suggestedFilename: String,
val mediaType: MediaType,
val format: Format,
val licenseDocument: LicenseDocument
) {
@Deprecated(
Expand All @@ -164,8 +163,8 @@ public interface LcpService {
*/
public operator fun invoke(
context: Context,
assetRetriever: AssetRetriever,
mediaTypeRetriever: MediaTypeRetriever,
assetOpener: AssetOpener,
assetSniffer: AssetSniffer,
downloadManager: DownloadManager
): LcpService? {
if (!LcpClient.isAvailable()) {
Expand All @@ -176,7 +175,7 @@ public interface LcpService {
val deviceRepository = DeviceRepository(db)
val passphraseRepository = PassphrasesRepository(db)
val licenseRepository = LicensesRepository(db)
val network = NetworkService(mediaTypeRetriever)
val network = NetworkService()
val device = DeviceService(
repository = deviceRepository,
network = network,
Expand All @@ -191,8 +190,8 @@ public interface LcpService {
network = network,
passphrases = passphrases,
context = context,
assetRetriever = assetRetriever,
mediaTypeRetriever = mediaTypeRetriever,
assetOpener = assetOpener,
assetSniffer = assetSniffer,
downloadManager = downloadManager
)
}
Expand All @@ -203,7 +202,7 @@ public interface LcpService {
ReplaceWith("LcpService(context, AssetRetriever(), MediaTypeRetriever())"),
level = DeprecationLevel.ERROR
)
public fun create(context: Context): LcpService? = throw NotImplementedError()
public fun create(context: Context): LcpService = throw NotImplementedError()
}

@Deprecated(
Expand Down
Loading

0 comments on commit bfadfa8

Please sign in to comment.