Skip to content

Commit

Permalink
Refactor Url and Href (#388)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu authored Sep 19, 2023
1 parent 35f00bb commit 9fe0d30
Show file tree
Hide file tree
Showing 232 changed files with 6,511 additions and 3,588 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.util.getOrThrow
import org.readium.r2.shared.util.pdf.PdfDocument
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
import org.readium.r2.shared.util.toFile
import org.readium.r2.shared.util.use
import timber.log.Timber

Expand Down
2 changes: 1 addition & 1 deletion readium/lcp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ dependencies {
exclude(module = "support-v4")
}
implementation(libs.joda.time)
implementation("org.zeroturnaround:zt-zip:1.15")
implementation(libs.androidx.browser)

implementation(libs.bundles.room)
ksp(libs.androidx.room.compiler)

// Tests
testImplementation(libs.junit)
testImplementation(libs.kotlin.junit)

androidTestImplementation(libs.androidx.ext.junit)
androidTestImplementation(libs.androidx.expresso.core)
Expand Down
118 changes: 55 additions & 63 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@ import org.readium.r2.shared.publication.encryption.encryption
import org.readium.r2.shared.publication.flatten
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
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.resource.TransformingContainer
import org.readium.r2.shared.util.AbsoluteUrl
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.flatMap
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.util.toFile

internal class LcpContentProtection(
private val lcpService: LcpService,
Expand All @@ -46,7 +43,7 @@ internal class LcpContentProtection(
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
): Try<ContentProtection.Asset, Publication.OpenError> {
return when (asset) {
is Asset.Container -> openPublication(asset, credentials, allowUserInteraction, sender)
is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction, sender)
Expand All @@ -58,7 +55,7 @@ internal class LcpContentProtection(
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
): Try<ContentProtection.Asset, Publication.OpenError> {
val license = retrieveLicense(asset, credentials, allowUserInteraction, sender)
return createResultAsset(asset, license)
}
Expand All @@ -73,27 +70,13 @@ internal class LcpContentProtection(
?.let { LcpPassphraseAuthentication(it, fallback = this.authentication) }
?: this.authentication

val file = (asset as? Asset.Resource)?.resource?.source?.toFile()
?: (asset as? Asset.Container)?.container?.source?.toFile()

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)
return lcpService.retrieveLicense(asset, authentication, allowUserInteraction, sender)
}

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

Expand All @@ -107,7 +90,9 @@ internal class LcpContentProtection(
onCreatePublication = {
decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links)
.flatten()
.mapNotNull { it.properties.encryption?.let { enc -> it.href to enc } }
.mapNotNull {
it.properties.encryption?.let { enc -> it.url() to enc }
}
.toMap()

servicesBuilder.contentProtectionServiceFactory = serviceFactory
Expand All @@ -122,7 +107,7 @@ internal class LcpContentProtection(
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
): Try<ContentProtection.Asset, Publication.OpenError> {
val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction, sender)

val licenseDoc = license.getOrNull()?.license
Expand All @@ -132,9 +117,7 @@ internal class LcpContentProtection(
LicenseDocument(it)
} catch (e: Exception) {
return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(e)
)
Publication.OpenError.InvalidAsset(cause = ThrowableError(e))
)
}
}
Expand All @@ -145,55 +128,64 @@ internal class LcpContentProtection(
}

val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.Publication))
val url = Url(link.url.toString())
val url = (link.url() as? AbsoluteUrl)
?: return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(
Publication.OpenError.InvalidAsset(
cause = ThrowableError(
LcpException.Parsing.Url(rel = LicenseDocument.Rel.Publication.value)
)
)
)

return assetRetriever.retrieve(
url,
mediaType = link.mediaType,
assetType = AssetType.Archive
)
.mapFailure { Publication.OpeningException.ParsingFailed(it) }
.flatMap { createResultAsset(it as Asset.Container, 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()
}
val asset =
if (link.mediaType != null) {
assetRetriever.retrieve(
url,
mediaType = link.mediaType,
assetType = AssetType.Archive
)
.map { it as Asset.Container }
.mapFailure { it.wrap() }
} else {
(assetRetriever.retrieve(url) as? Asset.Container)
?.let { Try.success(it) }
?: Try.failure(Publication.OpenError.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()
}
return asset.flatMap { createResultAsset(it, license) }
}

private fun Resource.Exception.wrap(): Publication.OpeningException =
private fun Resource.Exception.wrap(): Publication.OpenError =
when (this) {
is Resource.Exception.Forbidden ->
Publication.OpeningException.Forbidden(ThrowableError(this))
Publication.OpenError.Forbidden(ThrowableError(this))
is Resource.Exception.NotFound ->
Publication.OpeningException.NotFound(ThrowableError(this))
Publication.OpenError.NotFound(ThrowableError(this))
Resource.Exception.Offline, is Resource.Exception.Unavailable ->
Publication.OpeningException.Unavailable(ThrowableError(this))
Publication.OpenError.Unavailable(ThrowableError(this))
is Resource.Exception.Other, is Resource.Exception.BadRequest ->
Publication.OpeningException.Unexpected(this)
Publication.OpenError.Unknown(this)
is Resource.Exception.OutOfMemory ->
Publication.OpeningException.OutOfMemory(ThrowableError(this))
Publication.OpenError.OutOfMemory(ThrowableError(this))
}

private fun AssetRetriever.Error.wrap(): Publication.OpenError =
when (this) {
is AssetRetriever.Error.ArchiveFormatNotSupported ->
Publication.OpenError.UnsupportedAsset(this)
is AssetRetriever.Error.Forbidden ->
Publication.OpenError.Forbidden(this)
is AssetRetriever.Error.InvalidAsset ->
Publication.OpenError.InvalidAsset(this)
is AssetRetriever.Error.NotFound ->
Publication.OpenError.NotFound(this)
is AssetRetriever.Error.OutOfMemory ->
Publication.OpenError.OutOfMemory(this)
is AssetRetriever.Error.SchemeNotSupported ->
Publication.OpenError.UnsupportedAsset(this)
is AssetRetriever.Error.Unavailable ->
Publication.OpenError.Unavailable(this)
is AssetRetriever.Error.Unknown ->
Publication.OpenError.Unknown(this)
}
}
7 changes: 4 additions & 3 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.readium.r2.shared.resource.TransformingResource
import org.readium.r2.shared.resource.flatMap
import org.readium.r2.shared.resource.flatMapCatching
import org.readium.r2.shared.resource.mapCatching
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.getOrElse
Expand All @@ -32,7 +33,7 @@ import org.readium.r2.shared.util.getOrThrow
*/
internal class LcpDecryptor(
val license: LcpLicense?,
var encryptionData: Map<String, Encryption> = emptyMap()
var encryptionData: Map<Url, Encryption> = emptyMap()
) {

fun transform(resource: Resource): Resource {
Expand All @@ -41,7 +42,7 @@ internal class LcpDecryptor(
}

return resource.flatMap {
val encryption = encryptionData[resource.path]
val encryption = encryptionData[resource.url]

// Checks if the resource is encrypted and whether the encryption schemes of the resource
// and the DRM license are the same.
Expand Down Expand Up @@ -93,7 +94,7 @@ internal class LcpDecryptor(
private val license: LcpLicense
) : Resource by resource {

override val source: Url? = null
override val source: AbsoluteUrl? = null

private class Cache(
var startIndex: Int? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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.lcp

import android.content.Context
import java.io.File
import java.util.LinkedList
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.readium.r2.shared.util.CoroutineQueue

internal class LcpDownloadsRepository(
context: Context
) {
private val queue = CoroutineQueue()

private val storageDir: Deferred<File> =
queue.scope.async {
withContext(Dispatchers.IO) {
File(context.noBackupFilesDir, LcpDownloadsRepository::class.qualifiedName!!)
.also { if (!it.exists()) it.mkdirs() }
}
}

private val storageFile: Deferred<File> =
queue.scope.async {
withContext(Dispatchers.IO) {
File(storageDir.await(), "licenses.json")
.also { if (!it.exists()) { it.writeText("{}", Charsets.UTF_8) } }
}
}

private val snapshot: Deferred<MutableMap<String, JSONObject>> =
queue.scope.async {
readSnapshot().toMutableMap()
}

fun addDownload(id: String, license: JSONObject) {
queue.scope.launch {
val snapshotCompleted = snapshot.await()
snapshotCompleted[id] = license
writeSnapshot(snapshotCompleted)
}
}

fun removeDownload(id: String) {
queue.launch {
val snapshotCompleted = snapshot.await()
snapshotCompleted.remove(id)
writeSnapshot(snapshotCompleted)
}
}

suspend fun retrieveLicense(id: String): JSONObject? =
queue.await {
snapshot.await()[id]
}

private suspend fun readSnapshot(): Map<String, JSONObject> {
return withContext(Dispatchers.IO) {
storageFile.await().readText(Charsets.UTF_8).toData().toMutableMap()
}
}

private suspend fun writeSnapshot(snapshot: Map<String, JSONObject>) {
val storageFileCompleted = storageFile.await()
withContext(Dispatchers.IO) {
storageFileCompleted.writeText(snapshot.toJson(), Charsets.UTF_8)
}
}

private fun Map<String, JSONObject>.toJson(): String {
val jsonObject = JSONObject()
for ((id, license) in this.entries) {
jsonObject.put(id, license)
}
return jsonObject.toString()
}

private fun String.toData(): Map<String, JSONObject> {
val jsonObject = JSONObject(this)
val names = jsonObject.keys().iterator().toList()
return names.associateWith { jsonObject.getJSONObject(it) }
}

private fun <T> Iterator<T>.toList(): List<T> =
LinkedList<T>().apply {
while (hasNext())
this += next()
}.toMutableList()
}
7 changes: 4 additions & 3 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.annotation.StringRes
import java.net.SocketTimeoutException
import java.util.*
import org.readium.r2.shared.UserException
import org.readium.r2.shared.util.Url

public sealed class LcpException(
userMessageId: Int,
Expand Down Expand Up @@ -203,17 +204,17 @@ public sealed class LcpException(
public object OpenFailed : Container(R.string.readium_lcp_exception_container_open_failed)

/** The file at given relative path is not found in the Container. */
public class FileNotFound(public val path: String) : Container(
public class FileNotFound(public val url: Url) : Container(
R.string.readium_lcp_exception_container_file_not_found
)

/** Can't read the file at given relative path in the Container. */
public class ReadFailed(public val path: String) : Container(
public class ReadFailed(public val url: Url?) : Container(
R.string.readium_lcp_exception_container_read_failed
)

/** Can't write the file at given relative path in the Container. */
public class WriteFailed(public val path: String) : Container(
public class WriteFailed(public val url: Url?) : Container(
R.string.readium_lcp_exception_container_write_failed
)
}
Expand Down
Loading

0 comments on commit 9fe0d30

Please sign in to comment.