Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various changes including introducing Format #427

Merged
merged 88 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
10fe08e
Revisit factories
qnga Oct 24, 2023
8fa923c
WIP
qnga Oct 24, 2023
8244e92
WIP
qnga Oct 26, 2023
0d433c4
WIP
qnga Oct 29, 2023
d35bd5c
WIP
qnga Oct 31, 2023
dec440a
Refactor ArchiveFactory
qnga Nov 3, 2023
9b80755
Refactor container and resource
qnga Nov 6, 2023
524d9cc
Various
qnga Nov 15, 2023
356cf8a
Introduce HttpStatus
qnga Nov 16, 2023
14b6f48
Remove entries
qnga Nov 16, 2023
06d64f7
Get rid of Other errors
qnga Nov 17, 2023
88ffc01
Update error handling in testapp
qnga Nov 17, 2023
dbd73a5
Lint
qnga Nov 17, 2023
2ce91e0
Refactor LcpException and move UserError to test-app
qnga Nov 19, 2023
6d5b5a3
Various changes
qnga Nov 20, 2023
69d9dd9
Fix uses of Try.assertSuccess
qnga Nov 20, 2023
e720ebc
Fix PsPDFKit errors
qnga Nov 20, 2023
7053cd3
Small fixes
qnga Nov 20, 2023
e25514d
Small fixes
qnga Nov 20, 2023
d89a6dd
Fix sniffing and AssetRetriever
qnga Nov 20, 2023
b683e06
Fix SingleResourceContainer
qnga Nov 20, 2023
c99001e
Optimize ZIP sniffing
qnga Nov 20, 2023
5257109
Remove ReadError.Other
qnga Nov 21, 2023
7d55985
Add media type and filename properties on Resource
qnga Nov 21, 2023
3c77ec1
Fix
qnga Nov 21, 2023
6b200b1
Remove MediaTypeRetriever from EpubParser
qnga Nov 23, 2023
efb5671
Clarify and move MediaTypeRetriever
qnga Nov 23, 2023
1d85516
Refactor ArchiveFactory
qnga Nov 23, 2023
2df5555
Introduce BlobMediaTypeRetriever
qnga Nov 23, 2023
d92f49d
Remove passwords from ArchiveFactory
qnga Nov 27, 2023
6b3688c
Cosmetic change
qnga Nov 27, 2023
58a9846
Remove Resource.mediaType()
qnga Nov 27, 2023
1b167a4
Fix most of the tests
qnga Nov 28, 2023
268c3f5
Cosmetic changes
qnga Nov 28, 2023
8ec36a8
Cosmetic changes
qnga Nov 29, 2023
026c4d7
Merge branch 'v3' into refactor-errors
qnga Nov 29, 2023
7b677d8
Various fixes
qnga Nov 29, 2023
4b6b77f
Renaming
qnga Nov 29, 2023
881772e
Naming
qnga Nov 29, 2023
41ebe8f
Moves
qnga Nov 29, 2023
faea60c
Cosmetic changes
qnga Nov 29, 2023
c0d3f86
Cosmetic changes
qnga Nov 29, 2023
8f4002f
Small fixes
qnga Nov 29, 2023
fd211ad
Remove FLAG_SECURE
mickael-menu Nov 30, 2023
1d7f896
Fix ReadableInputStream lifecycle issues
qnga Nov 30, 2023
57d5183
Rename OverflowableNavigator
qnga Nov 30, 2023
6755380
Cosmetic changes
qnga Nov 30, 2023
1b05cea
Move media type retriever back to `mediatype` package
mickael-menu Dec 1, 2023
61db1b1
Various fixes and cosmetic changes
qnga Dec 5, 2023
cb567c9
Minor changes
qnga Dec 6, 2023
16eca61
Fix SearchService errors
qnga Dec 6, 2023
2adb5c8
Fix ExoMediaPlayer
qnga Dec 6, 2023
101017f
Refactor BufferingResource
qnga Dec 6, 2023
b25739b
Cosmetic changes
qnga Dec 6, 2023
296c722
Fix LcpFallbackContentProtection
qnga Dec 6, 2023
10a1f5d
Improve MediaTypeRetriever hint sniffing efficiency
qnga Dec 6, 2023
678238b
Cosmetic changes
qnga Dec 6, 2023
77f11a7
Reorganization
qnga Dec 6, 2023
db48f8d
Doc
qnga Dec 6, 2023
632c8ca
Cosmetic changes
qnga Dec 6, 2023
fce3d33
Cosmetic changes
qnga Dec 6, 2023
ec16df2
Refactor decoding
qnga Dec 7, 2023
14a1456
Clean up decoding
qnga Dec 8, 2023
565df38
Cosmetic changes
qnga Dec 8, 2023
379bac0
Refactor user errors in testapp
qnga Dec 8, 2023
cc19717
Fix DownloadManager errors
qnga Dec 8, 2023
7100890
Small fix
qnga Dec 8, 2023
ce4dbd5
Small fix
qnga Dec 8, 2023
f65b677
Various changes
qnga Dec 8, 2023
c8e0066
Minor changes
mickael-menu Dec 10, 2023
e225659
Keep `HttpRequest.Builder.url` immutable
mickael-menu Dec 11, 2023
c1db83a
WIP
qnga Dec 10, 2023
543b7ab
Organisation
qnga Dec 10, 2023
d924a05
Fail on reading errors in Adept and fallback Lcp sniffing
qnga Dec 11, 2023
fa605cb
Fix tests
qnga Dec 11, 2023
f20b321
Add caching
qnga Dec 13, 2023
4f959a1
Remove ParserAssetFactory
qnga Dec 13, 2023
579e60e
Remove ParserAssetFactory
qnga Dec 13, 2023
7ac223e
Fix tests
qnga Dec 17, 2023
921fe10
Fill encryption data in LcpContentProtection
qnga Dec 18, 2023
90ea48f
Merge branch 'v3' into new-format
qnga Dec 18, 2023
cec6405
Remove scheme in ContentProtection
qnga Dec 18, 2023
b07488f
Various changes
qnga Dec 19, 2023
c68016e
Fix sniffing
qnga Dec 19, 2023
153736a
Use FileExtension in Url.extension
qnga Dec 19, 2023
365b9ae
Various changes
qnga Dec 20, 2023
76a9543
Various changes
qnga Dec 21, 2023
07a6f25
Changes
qnga Dec 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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